mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ea49d486b | |||
| 4d4fc80f25 | |||
| dfa56c37b4 | |||
| 4249de5e23 | |||
| 536bbdf73f | |||
| 201efbd07f | |||
| 8ae078b4d7 | |||
| d84b173330 | |||
| 33d654e7d9 | |||
| 6eb100cc41 | |||
| dca1ef09cb | |||
| 616c944350 | |||
| c0f919310e | |||
| 92a09c8d8d | |||
| ecae62e25a | |||
| abb4583554 | |||
| fc7a269f0a | |||
| 9c85ef1fda | |||
| bd60ba9e68 | |||
| 2e466bd4cb | |||
| 55778583b9 | |||
| 661bd39362 | |||
| 04eb2ee9a3 | |||
| c6f6fb00c9 | |||
| 02eff4efb3 | |||
| db9eef50ef |
@@ -0,0 +1,60 @@
|
||||
description = "Check status of nightly evals, fix failures for key models, and re-run."
|
||||
prompt = """
|
||||
You are an expert at fixing behavioral evaluations.
|
||||
|
||||
1. **Investigate**:
|
||||
- Use 'gh' cli to fetch the results from the latest run from the main branch: https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml.
|
||||
- DO NOT push any changes or start any runs. The rest of your evaluation will be local.
|
||||
- Evals are in evals/ directory and are documented by evals/README.md.
|
||||
- The test case trajectory logs will be logged to evals/logs.
|
||||
- You should also enable and review the verbose agent logs by setting the GEMINI_DEBUG_LOG_FILE environment variable.
|
||||
- Identify the relevant test. Confine your investigation and validation to just this test.
|
||||
- Proactively add logging that will aid in gathering information or validating your hypotheses.
|
||||
|
||||
2. **Fix**:
|
||||
- If a relevant test is failing, locate the test file and the corresponding prompt/code.
|
||||
- It's often helpful to make an extreme, brute force change to see if you are changing the right place to make an improvement and then scope it back iteratively.
|
||||
- Your **final** change should be **minimal and targeted**.
|
||||
- Keep in mind the following:
|
||||
- The prompt has multiple configurations and pieces. Take care that your changes
|
||||
end up in the final prompt for the selected model and configuration.
|
||||
- The prompt chosen for the eval is intentional. It's often vague or indirect
|
||||
to see how the agent performs with ambiguous instructions. Changing it should
|
||||
be a last resort.
|
||||
- When changing the test prompt, carefully consider whether the prompt still tests
|
||||
the same scenario. We don't want to lose test fidelity by making the prompts too
|
||||
direct (i.e.: easy).
|
||||
- Your primary mechanism for improving the agent's behavior is to make changes to
|
||||
tool instructions, system prompt (snippets.ts), and/or modules that contribute to the prompt.
|
||||
- If prompt and description changes are unsuccessful, use logs and debugging to
|
||||
confirm that everything is working as expected.
|
||||
- If unable to fix the test, you can make recommendations for architecture changes
|
||||
that might help stablize the test. Be sure to THINK DEEPLY if offering architecture guidance.
|
||||
Some facts that might help with this are:
|
||||
- Agents may be composed of one or more agent loops.
|
||||
- AgentLoop == 'context + toolset + prompt'. Subagents are one type of agent loop.
|
||||
- Agent loops perform better when:
|
||||
- They have direct, unambiguous, and non-contradictory prompts.
|
||||
- They have fewer irrelevant tools.
|
||||
- They have fewer goals or steps to perform.
|
||||
- They have less low value or irrelevant context.
|
||||
- You may suggest compositions of existing primitives, like subagents, or
|
||||
propose a new one.
|
||||
- These recommendations should be high confidence and should be grounded
|
||||
in observed deficient behaviors rather than just parroting the facts above.
|
||||
Investigate as needed to ground your recommendations.
|
||||
|
||||
3. **Verify**:
|
||||
- Run just that one test if needed to validate that it is fixed. Be sure to run vitest in non-interactive mode.
|
||||
- Running the tests can take a long time, so consider whether you can diagnose via other means or log diagnostics before committing the time. You must minimize the number of test runs needed to diagnose the failure.
|
||||
- After the test completes, check whether it seems to have improved.
|
||||
- You will need to run the test 3 times for Gemini 3.0, Gemini 3 flash, and Gemini 2.5 pro to ensure that it is truly stable. Run these runs in parallel, using scripts if needed.
|
||||
- Some flakiness is expected; if it looks like a transient issue or the test is inherently unstable but passes 2/3 times, you might decide it cannot be improved.
|
||||
|
||||
4. **Report**:
|
||||
- Provide a summary of the test success rate for each of the tested models.
|
||||
- Success rate is calculated based on 3 runs per model (e.g., 3/3 = 100%).
|
||||
- If you couldn't fix it due to persistent flakiness, explain why.
|
||||
|
||||
{{args}}
|
||||
"""
|
||||
@@ -0,0 +1,29 @@
|
||||
description = "Promote behavioral evals that have a 100% success rate over the last 7 nightly runs."
|
||||
prompt = """
|
||||
You are an expert at analyzing and promoting behavioral evaluations.
|
||||
|
||||
1. **Investigate**:
|
||||
- Use 'gh' cli to fetch the results from the most recent run from the main branch: https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml.
|
||||
- DO NOT push any changes or start any runs. The rest of your evaluation will be local.
|
||||
- Evals are in evals/ directory and are documented by evals/README.md.
|
||||
- Identify tests that have passed 100% of the time for ALL enabled models across the past 7 runs in a row.
|
||||
- NOTE: the results summary from the most recent run contains the last 7 runs test results. 100% means the test passed 3/3 times for that model and run.
|
||||
- If a test meets this criteria, it is a candidate for promotion.
|
||||
|
||||
2. **Promote**:
|
||||
- For each candidate test, locate the test file in the evals/ directory.
|
||||
- Promote the test according to the project's standard promotion process (e.g., moving it to a stable suite, updating its tags, or removing skip/flaky annotations).
|
||||
- Ensure you follow any guidelines in evals/README.md for stable tests.
|
||||
- Your **final** change should be **minimal and targeted** to just promoting the test status.
|
||||
|
||||
3. **Verify**:
|
||||
- Run the promoted tests locally to validate that they still execute correctly. Be sure to run vitest in non-interactive mode.
|
||||
- Check that the test is now part of the expected standard or stable test suites.
|
||||
|
||||
4. **Report**:
|
||||
- Provide a summary of the tests that were promoted.
|
||||
- Include the success rate evidence (7/7 runs passed for all models) for each promoted test.
|
||||
- If no tests met the criteria for promotion, clearly state that and summarize the closest candidates.
|
||||
|
||||
{{args}}
|
||||
"""
|
||||
@@ -7,5 +7,15 @@
|
||||
},
|
||||
"general": {
|
||||
"devtools": true
|
||||
},
|
||||
"agents": {
|
||||
"overrides": {
|
||||
"browser_agent": { "enabled": true }
|
||||
},
|
||||
"browser": {
|
||||
"headless": true,
|
||||
"sessionMode": "isolated",
|
||||
"allowedDomains": ["*.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
name: behavioral-evals
|
||||
description: Guidance for creating, running, fixing, and promoting behavioral evaluations. Use when verifying agent decision logic, debugging failures, debugging prompt steering, or adding workspace regression tests.
|
||||
---
|
||||
|
||||
# Behavioral Evals
|
||||
|
||||
## Overview
|
||||
|
||||
Behavioral evaluations (evals) are tests that validate the **agent's decision-making** (e.g., tool choice) rather than pure functionality. They are critical for verifying prompt changes, debugging steerability, and preventing regressions.
|
||||
|
||||
> [!NOTE]
|
||||
> **Single Source of Truth**: For core concepts, policies, running tests, and general best practices, always refer to **[evals/README.md](file:///Users/abhipatel/code/gemini-cli/docs/evals/README.md)**.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Workflow Decision Tree
|
||||
|
||||
1. **Does a prompt/tool change need validation?**
|
||||
* *No* -> Normal integration tests.
|
||||
* *Yes* -> Continue below.
|
||||
2. **Is it UI/Interaction heavy?**
|
||||
* *Yes* -> Use `appEvalTest` (`AppRig`). See **[creating.md](references/creating.md)**.
|
||||
* *No* -> Use `evalTest` (`TestRig`). See **[creating.md](references/creating.md)**.
|
||||
3. **Is it a new test?**
|
||||
* *Yes* -> Set policy to `USUALLY_PASSES`.
|
||||
* *No* -> `ALWAYS_PASSES` (locks in regression).
|
||||
4. **Are you fixing a failure or promoting a test?**
|
||||
* *Fixing* -> See **[fixing.md](references/fixing.md)**.
|
||||
* *Promoting* -> See **[promoting.md](references/promoting.md)**.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Checklist
|
||||
|
||||
### 1. Setup Workspace
|
||||
Seed the workspace with necessary files using the `files` object to simulate a realistic scenario (e.g., NodeJS project with `package.json`).
|
||||
* *Details in **[creating.md](references/creating.md)***
|
||||
|
||||
### 2. Write Assertions
|
||||
Audit agent decisions using `rig.setBreakpoint()` (AppRig only) or index verification on `rig.readToolLogs()`.
|
||||
* *Details in **[creating.md](references/creating.md)***
|
||||
|
||||
### 3. Verify
|
||||
Run single tests locally with Vitest. Confirm stability locally before relying on CI workflows.
|
||||
* *See **[evals/README.md](file:///Users/abhipatel/code/gemini-cli/docs/evals/README.md)** for running commands.*
|
||||
|
||||
---
|
||||
|
||||
## 📦 Bundled Resources
|
||||
|
||||
Detailed procedural guides:
|
||||
* **[creating.md](references/creating.md)**: Assertion strategies, Rig selection, Mock MCPs.
|
||||
* **[fixing.md](references/fixing.md)**: Step-by-step automated investigation, architecture diagnosis guidelines.
|
||||
* **[promoting.md](references/promoting.md)**: Candidate identification criteria and threshold guidelines.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { appEvalTest } from './app-test-helper.js';
|
||||
|
||||
describe('interactive_feature', () => {
|
||||
// New tests MUST start as USUALLY_PASSES
|
||||
appEvalTest('USUALLY_PASSES', {
|
||||
name: 'should pause for user confirmation',
|
||||
files: {
|
||||
'package.json': JSON.stringify({ name: 'app' })
|
||||
},
|
||||
prompt: 'Task description here requiring approval',
|
||||
timeout: 60000,
|
||||
setup: async (rig) => {
|
||||
// ⚠️ Breakpoints are ONLY safe in appEvalTest
|
||||
rig.setBreakpoint(['ask_user']);
|
||||
},
|
||||
assert: async (rig) => {
|
||||
// 1. Wait for the breakpoint to trigger
|
||||
const confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||
expect(confirmation).toBeDefined();
|
||||
|
||||
// 2. Resolve it so the test can finish
|
||||
await rig.resolveTool(confirmation);
|
||||
await rig.waitForIdle();
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('core_feature', () => {
|
||||
// New tests MUST start as USUALLY_PASSES
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should perform expected agent action',
|
||||
setup: async (rig) => {
|
||||
// For mocking offline MCP:
|
||||
// rig.addMockMcpServer('workspace-server', 'google-workspace');
|
||||
},
|
||||
files: {
|
||||
'src/app.ts': '// some code',
|
||||
},
|
||||
prompt: 'Task description here',
|
||||
timeout: 60000, // 1 minute safety limit
|
||||
assert: async (rig, result) => {
|
||||
// 1. Audit the trajectory (Safe for standard evalTest)
|
||||
const logs = rig.readToolLogs();
|
||||
const hasTool = logs.some((l) => l.toolRequest.name === 'read_file');
|
||||
expect(hasTool, 'Agent should have read the file').toBe(true);
|
||||
|
||||
// 2. Assert efficiency (Cost/Turn)
|
||||
expect(logs.length).toBeLessThan(5);
|
||||
|
||||
// 3. Assert final output
|
||||
expect(result).toContain('Expected Keyword');
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,151 +0,0 @@
|
||||
# Creating Behavioral Evals
|
||||
|
||||
## 🔬 Rig Selection
|
||||
|
||||
| Rig Type | Import From | Architecture | Use When |
|
||||
| :---------------- | :--------------------- | :------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- |
|
||||
| **`evalTest`** | `./test-helper.js` | **Subprocess**. Runs the CLI in a separate process + waits for exit. | Standard workspace tests. **Do not use `setBreakpoint`**; auditing history (`readToolLogs`) is safer. |
|
||||
| **`appEvalTest`** | `./app-test-helper.js` | **In-Process**. Runs directly inside the runner loop. | UI/Ink rendering. Safe for `setBreakpoint` triggers. |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Scenario Design
|
||||
|
||||
Evals must simulate realistic agent environments to effectively test
|
||||
decision-making.
|
||||
|
||||
- **Workspace State**: Seed with standard project anchors if testing general
|
||||
capabilities:
|
||||
- `package.json` for NodeJS environments.
|
||||
- Minimal configuration files (`tsconfig.json`, `GEMINI.md`).
|
||||
- **Structural Complexity**: Provide enough files to force the agent to _search_
|
||||
or _navigate_, rather than giving the answer directly. Avoid trivial one-file
|
||||
tests unless testing exact prompt steering.
|
||||
|
||||
---
|
||||
|
||||
## ❌ Fail First Principle
|
||||
|
||||
Before asserting a new capability or locking in a fix, **verify that the test
|
||||
fails first**.
|
||||
|
||||
- It is easy to accidentally write an eval that asserts behaviors that are
|
||||
already met or pass by default.
|
||||
- **Process**: reproduce failure with test -> apply fix (prompt/tool) -> verify
|
||||
test passes.
|
||||
|
||||
---
|
||||
|
||||
## ✋ Testing Patterns
|
||||
|
||||
### 1. Breakpoints
|
||||
|
||||
Verifies the agent _intends_ to use a tool BEFORE executing it. Useful for
|
||||
interactive prompts or safety checks.
|
||||
|
||||
```typescript
|
||||
// ⚠️ Only works with appEvalTest (AppRig)
|
||||
setup: async (rig) => {
|
||||
rig.setBreakpoint(['ask_user']);
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||
expect(confirmation).toBeDefined();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Tool Confirmation Race
|
||||
|
||||
When asserting multiple triggers (e.g., "enters plan mode then asks question"):
|
||||
|
||||
```typescript
|
||||
assert: async (rig) => {
|
||||
let confirmation = await rig.waitForPendingConfirmation([
|
||||
'enter_plan_mode',
|
||||
'ask_user',
|
||||
]);
|
||||
|
||||
if (confirmation?.name === 'enter_plan_mode') {
|
||||
rig.acceptConfirmation('enter_plan_mode');
|
||||
confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||
}
|
||||
expect(confirmation?.toolName).toBe('ask_user');
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Audit Tool Logs
|
||||
|
||||
Audit exact operations to ensure efficiency (e.g., no redundant reads).
|
||||
|
||||
```typescript
|
||||
assert: async (rig, result) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
|
||||
const writeCall = toolLogs.find(
|
||||
(log) => log.toolRequest.name === 'write_file',
|
||||
);
|
||||
expect(writeCall).toBeDefined();
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Mock MCP Facades
|
||||
|
||||
To evaluate tools connected via MCP without hitting live endpoints, load a mock
|
||||
server configuration in the `setup` hook.
|
||||
|
||||
```typescript
|
||||
setup: async (rig) => {
|
||||
rig.addMockMcpServer('workspace-server', 'google-workspace');
|
||||
},
|
||||
assert: async (rig) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const workspaceCall = toolLogs.find(
|
||||
(log) => log.toolRequest.name === 'mcp_workspace-server_docs.getText'
|
||||
);
|
||||
expect(workspaceCall).toBeDefined();
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Safety & Efficiency Guardrails
|
||||
|
||||
### 1. Breakpoint Deadlocks
|
||||
|
||||
Breakpoints (`setBreakpoint`) pause execution. In standard `evalTest`,
|
||||
`rig.run()` waits for the process to exit _before_ assertions run. **This will
|
||||
hang indefinitely.**
|
||||
|
||||
- **Use Breakpoints** for `appEvalTest` or interactive simulations.
|
||||
- **Use Audit Tool Logs** (above) for standard trajectory tests.
|
||||
|
||||
### 2. Runaway Timeout
|
||||
|
||||
Always set a budget boundary in the `EvalCase` to prevent runaway loops on
|
||||
quota:
|
||||
|
||||
```typescript
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: '...',
|
||||
timeout: 60000, // 1 minute safety limit
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Efficiency Assertion (Turn limits)
|
||||
|
||||
Check if a tool is called _early_ using index checks:
|
||||
|
||||
```typescript
|
||||
assert: async (rig) => {
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const toolCallIndex = toolLogs.findIndex(
|
||||
(log) => log.toolRequest.name === 'cli_help',
|
||||
);
|
||||
|
||||
expect(toolCallIndex).toBeGreaterThan(-1);
|
||||
expect(toolCallIndex).toBeLessThan(5); // Called within first 5 turns
|
||||
};
|
||||
```
|
||||
@@ -1,71 +0,0 @@
|
||||
# Fixing Behavioral Evals
|
||||
|
||||
Use this guide when asked to debug, troubleshoot, or fix a failing behavioral
|
||||
evaluation.
|
||||
|
||||
---
|
||||
|
||||
## 1. 🔍 Investigate
|
||||
|
||||
1. **Fetch Nightly Results**: Use the `gh` CLI to inspect the latest run from
|
||||
`evals-nightly.yml` if applicable.
|
||||
- _Example view URL_:
|
||||
`https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml`
|
||||
2. **Isolate**: DO NOT push changes or start remote runs. Confine investigation
|
||||
to the local workspace.
|
||||
3. **Read Logs**:
|
||||
- Eval logs live in `evals/logs/<test_name>.log`.
|
||||
- Enable verbose debugging via `export GEMINI_DEBUG_LOG_FILE="debug.log"`.
|
||||
4. **Diagnose**: Audit tool logs and telemetry. Note if due to setup/assert.
|
||||
- **Tip**: Proactively add custom logging/diagnostics to check hypotheses.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🛠️ Fix Strategy
|
||||
|
||||
1. **Targeted Location**: Locate the test case and the corresponding
|
||||
prompt/code.
|
||||
2. **Iterative Scope**: Make extreme change first to verify scope, then refine
|
||||
to a minimal, targeted change.
|
||||
3. **Assertion Fidelity**:
|
||||
- Changing the test prompt is a **last resort** (prompts are often vague by
|
||||
design).
|
||||
- **Warning**: Do not lose test fidelity by making prompts too direct/easy.
|
||||
- **Primary Fix Trigger**: Adjust tool descriptions, system prompts
|
||||
(`snippets.ts`), or **modules that contribute to the prompt template**.
|
||||
- **Warning**: Prompts have multiple configurations; ensure your fix targets
|
||||
the correct config for the model in question.
|
||||
4. **Architecture Options**: If prompt or instruction tuning triggers no
|
||||
improvement, analyze loop composition.
|
||||
- **AgentLoop**: Defined by `context + toolset + prompt`.
|
||||
- **Enhancements**: Loops perform best with direct prompts, fewer irrelevant
|
||||
tools, low goal density, and minimal low-value/irrelevant context.
|
||||
- **Modifications**: Compose subagents or isolate tools. Ground in observed
|
||||
traces.
|
||||
- **Warning**: Think deeply before offering recommendations; avoid parroting
|
||||
abstract design guidelines.
|
||||
|
||||
---
|
||||
|
||||
## 3. ✅ Verify
|
||||
|
||||
1. **Run Local**: Run Vitest in non-interactive mode on just the file.
|
||||
2. **Log Audit**: Prioritize diagnosing failures via log comparison before
|
||||
triggering heavy test runs.
|
||||
3. **Stability Limit**: Run the test **3 times** locally on key models (can use
|
||||
scripts to run in parallel for speed):
|
||||
- **Gemini 3.0**
|
||||
- **Gemini 3 Flash**
|
||||
- **Gemini 2.5 Pro**
|
||||
4. **Flakiness Rule**: If it passes 2/3 times, it may be inherent noise
|
||||
difficult to improve without a structural split.
|
||||
|
||||
---
|
||||
|
||||
## 4. 📊 Report
|
||||
|
||||
Provide a summary of:
|
||||
|
||||
- Test success rate for each tested model (e.g., 3/3 = 100%).
|
||||
- Root cause identification and fix explanation.
|
||||
- If unfixed, provide high-confidence architecture recommendations.
|
||||
@@ -1,55 +0,0 @@
|
||||
# Promoting Behavioral Evals
|
||||
|
||||
Use this guide when asked to analyze nightly results and promote incubated tests
|
||||
to stable suites.
|
||||
|
||||
---
|
||||
|
||||
## 1. 🔍 Investigate candidates
|
||||
|
||||
1. **Audit Nightly Logs**: Use the `gh` CLI to fetch results from
|
||||
`evals-nightly.yml` (Direct URL:
|
||||
`https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml`).
|
||||
- **Tip**: The aggregate summary from the most recent run integrates the
|
||||
last 7 runs of history automatically.
|
||||
- **Safety**: DO NOT push changes or start remote runs. All verification is
|
||||
local.
|
||||
2. **Assess Stability**: Identify tests that pass **100% of the time** across
|
||||
ALL enabled models over the **last 7 nightly runs** in a row.
|
||||
- _100% means the test passed 3/3 times for every model and run._
|
||||
3. **Promotion Targets**: Tests meeting this criteria are candidates for
|
||||
promotion from `USUALLY_PASSES` to `ALWAYS_PASSES`.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🚥 Promotion Steps
|
||||
|
||||
1. **Locate File**: Locate the eval file in the `evals/` directory.
|
||||
2. **Update Policy**: Modify the policy argument to `ALWAYS_PASSES`.
|
||||
```typescript
|
||||
evalTest('ALWAYS_PASSES', { ... })
|
||||
```
|
||||
3. **Targeting**: Follow guidelines in `evals/README.md` regarding stable suite
|
||||
organization.
|
||||
4. **Constraint**: Your final change must be **minimal and targeted** strictly
|
||||
to promoting the test status. Do not refactor the test or setup fixtures.
|
||||
|
||||
---
|
||||
|
||||
## 3. ✅ Verify
|
||||
|
||||
1. **Run Prompted Tests**: Run the promoted test locally using non-interactive
|
||||
Vitest to confirm structure validity.
|
||||
2. **Verify Suite Inclusion**: Check that the test is successfully picked up by
|
||||
standard runnable ranges.
|
||||
|
||||
---
|
||||
|
||||
## 4. 📊 Report
|
||||
|
||||
Provide a summary of:
|
||||
|
||||
- Which tests were promoted.
|
||||
- Provide the success rate evidence (e.g., 7/7 runs passed for all models).
|
||||
- If no candidates qualified, list the next closest candidates and their current
|
||||
pass rate.
|
||||
@@ -1,95 +0,0 @@
|
||||
# Running & Promoting Evals
|
||||
|
||||
## 🛠️ Prerequisites
|
||||
|
||||
Behavioral evals run against the compiled binary. You **must** build and bundle
|
||||
the project first after making changes:
|
||||
|
||||
```bash
|
||||
npm run build && npm run bundle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏃♂️ Running Tests
|
||||
|
||||
### 1. Configure Environment Variables
|
||||
|
||||
Evals require a standard API key. If your `.env` file has multiple keys or
|
||||
comments, use this precise extraction setup:
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY=$(grep '^GEMINI_API_KEY=' .env | cut -d '=' -f2) && RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts <file_name>
|
||||
```
|
||||
|
||||
### 2. Commands
|
||||
|
||||
| Command | Scope | Description |
|
||||
| :---------------------------------- | :-------------- | :------------------------------------------------- |
|
||||
| `npm run test:always_passing_evals` | `ALWAYS_PASSES` | Fast feedback, runs in CI. |
|
||||
| `npm run test:all_evals` | All | Runs nightly incubation tests. Sets `RUN_EVALS=1`. |
|
||||
|
||||
### Target Specific File
|
||||
|
||||
_Note: `RUN_EVALS=1` is required for incubated (`USUALLY_PASSES`) tests._
|
||||
|
||||
```bash
|
||||
RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts my_feature.eval.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐞 Debugging and Logs
|
||||
|
||||
If a test fails, verify:
|
||||
|
||||
- **Tool Trajectory Logs**:序列 of calls in `evals/logs/<test_name>.log`.
|
||||
- **Verbose Reasoning**: Capture raw buffer traces by setting
|
||||
`GEMINI_DEBUG_LOG_FILE`:
|
||||
```bash
|
||||
export GEMINI_DEBUG_LOG_FILE="debug.log"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🎯 Verify Model Targeting
|
||||
|
||||
- **Tip:** Standard evals benchmark against model variations. If a test passes
|
||||
on Flash but fails on Pro (or vice versa), the issue is usually in the **tool
|
||||
description**, not the prompt definition. Flash is sensitive to "instruction
|
||||
bloat," while Pro is sensitive to "ambiguous intent."
|
||||
|
||||
---
|
||||
|
||||
## 🚥 deflaking & Promotion
|
||||
|
||||
To maintain CI stability, all new evals follow a strict incubation period.
|
||||
|
||||
### 1. Incubation (`USUALLY_PASSES`)
|
||||
|
||||
New tests must be created with the `USUALLY_PASSES` policy.
|
||||
|
||||
```typescript
|
||||
evalTest('USUALLY_PASSES', { ... })
|
||||
```
|
||||
|
||||
They run in **Evals: Nightly** workflows and do not block PR merges.
|
||||
|
||||
### 2. Investigate Failures
|
||||
|
||||
If a nightly eval regresses, investigate via agent:
|
||||
|
||||
```bash
|
||||
gemini /fix-behavioral-eval [optional-run-uri]
|
||||
```
|
||||
|
||||
### 3. Promotion (`ALWAYS_PASSES`)
|
||||
|
||||
Once a test scores 100% consistency over multiple nightly cycles:
|
||||
|
||||
```bash
|
||||
gemini /promote-behavioral-eval
|
||||
```
|
||||
|
||||
_Do not promote manually._ The command verifies trajectory logs before updating
|
||||
the file policy.
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
name: ci
|
||||
description:
|
||||
A specialized skill for Gemini CLI that provides high-performance, fail-fast
|
||||
monitoring of GitHub Actions workflows and automated local verification of CI
|
||||
failures. It handles run discovery automatically—simply provide the branch name.
|
||||
---
|
||||
|
||||
# CI Replicate & Status
|
||||
|
||||
This skill enables the agent to efficiently monitor GitHub Actions, triage
|
||||
failures, and bridge remote CI errors to local development. It defaults to
|
||||
**automatic replication** of failures to streamline the fix cycle.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Automatic Replication**: Automatically monitors CI and immediately executes
|
||||
suggested test or lint commands locally upon failure.
|
||||
- **Real-time Monitoring**: Aggregated status line for all concurrent workflows
|
||||
on the current branch.
|
||||
- **Fail-Fast Triage**: Immediately stops on the first job failure to provide a
|
||||
structured report.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. CI Replicate (`replicate`) - DEFAULT
|
||||
Use this as the primary path to monitor CI and **automatically** replicate
|
||||
failures locally for immediate triage and fixing.
|
||||
- **Behavior**: When this workflow is triggered, the agent will monitor the CI
|
||||
and **immediately and automatically execute** all suggested test or lint
|
||||
commands (marked with 🚀) as soon as a failure is detected.
|
||||
- **Tool**: `node .gemini/skills/ci/scripts/ci.mjs [branch]`
|
||||
- **Discovery**: The script **automatically** finds the latest active or recent
|
||||
run for the branch. Do NOT manually search for run IDs.
|
||||
- **Goal**: Reproduce the failure locally without manual intervention, then
|
||||
proceed to analyze and fix the code.
|
||||
|
||||
### 1. CI Status (`status`)
|
||||
Use this when you have pushed changes and need to monitor the CI and reproduce
|
||||
any failures locally.
|
||||
- **Tool**: `node .gemini/skills/ci/scripts/ci.mjs [branch] [run_id]`
|
||||
- **Discovery**: The script **automatically** finds the latest active or recent
|
||||
run for the branch. You should NOT manually search for \`run_id\` using \`gh run list\`
|
||||
unless a specific historical run is requested. Simply provide the branch name.
|
||||
- **Step 1 (Monitor)**: Execute the tool with the branch name.
|
||||
- **Step 2 (Extract)**: Extract suggested \`npm test\` or \`npm run lint\` commands
|
||||
from the output (marked with 🚀).
|
||||
- **Step 3 (Reproduce)**: Execute those commands locally to confirm the failure.
|
||||
- **Behavior**: It will poll every 15 seconds. If it detects a failure, it will
|
||||
exit with a structured report and provide the exact commands to run locally.
|
||||
|
||||
## Failure Categories & Actions
|
||||
|
||||
- **Test Failures**: Agent should run the specific `npm test -w <pkg> -- <path>`
|
||||
command suggested.
|
||||
- **Lint Errors**: Agent should run `npm run lint:all` or the specific package
|
||||
lint command.
|
||||
- **Build Errors**: Agent should check `tsc` output or build logs to resolve
|
||||
compilation issues.
|
||||
- **Job Errors**: Investigate `gh run view --job <job_id> --log` for
|
||||
infrastructure or setup failures.
|
||||
|
||||
## Noise Filtering
|
||||
The underlying scripts automatically filter noise (Git logs, NPM warnings, stack
|
||||
trace overhead). The agent should focus on the "Structured Failure Report"
|
||||
provided by the tool.
|
||||
@@ -1,281 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const BRANCH =
|
||||
process.argv[2] || execSync('git branch --show-current').toString().trim();
|
||||
const RUN_ID_OVERRIDE = process.argv[3];
|
||||
|
||||
let REPO;
|
||||
try {
|
||||
const remoteUrl = execSync('git remote get-url origin').toString().trim();
|
||||
REPO = remoteUrl
|
||||
.replace(/.*github\.com[\/:]/, '')
|
||||
.replace(/\.git$/, '')
|
||||
.trim();
|
||||
} catch (e) {
|
||||
REPO = 'google-gemini/gemini-cli';
|
||||
}
|
||||
|
||||
const FAILED_FILES = new Set();
|
||||
|
||||
function runGh(args) {
|
||||
try {
|
||||
return execSync(`gh ${args}`, {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).toString();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchFailuresViaApi(jobId) {
|
||||
try {
|
||||
const cmd = `gh api repos/${REPO}/actions/jobs/${jobId}/logs | grep -iE " FAIL |❌|ERROR|Lint failed|Build failed|Exception|failed with exit code"`;
|
||||
return execSync(cmd, {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
}).toString();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isNoise(line) {
|
||||
const lower = line.toLowerCase();
|
||||
return (
|
||||
lower.includes('* [new branch]') ||
|
||||
lower.includes('npm warn') ||
|
||||
lower.includes('fetching updates') ||
|
||||
lower.includes('node:internal/errors') ||
|
||||
lower.includes('at ') || // Stack traces
|
||||
lower.includes('checkexecsyncerror') ||
|
||||
lower.includes('node_modules')
|
||||
);
|
||||
}
|
||||
|
||||
function extractTestFile(failureText) {
|
||||
const cleanLine = failureText
|
||||
.replace(/[|#\[\]()]/g, ' ')
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.trim();
|
||||
const fileMatch = cleanLine.match(/([\w\/._-]+\.test\.[jt]sx?)/);
|
||||
if (fileMatch) return fileMatch[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
function generateTestCommand(failedFilesMap) {
|
||||
const workspaceToFiles = new Map();
|
||||
for (const [file, info] of failedFilesMap.entries()) {
|
||||
if (
|
||||
['Job Error', 'Unknown File', 'Build Error', 'Lint Error'].includes(file)
|
||||
)
|
||||
continue;
|
||||
let workspace = '@google/gemini-cli';
|
||||
let relPath = file;
|
||||
if (file.startsWith('packages/core/')) {
|
||||
workspace = '@google/gemini-cli-core';
|
||||
relPath = file.replace('packages/core/', '');
|
||||
} else if (file.startsWith('packages/cli/')) {
|
||||
workspace = '@google/gemini-cli';
|
||||
relPath = file.replace('packages/cli/', '');
|
||||
}
|
||||
relPath = relPath.replace(/^.*packages\/[^\/]+\//, '');
|
||||
if (!workspaceToFiles.has(workspace))
|
||||
workspaceToFiles.set(workspace, new Set());
|
||||
workspaceToFiles.get(workspace).add(relPath);
|
||||
}
|
||||
const commands = [];
|
||||
for (const [workspace, files] of workspaceToFiles.entries()) {
|
||||
commands.push(`npm test -w ${workspace} -- ${Array.from(files).join(' ')}`);
|
||||
}
|
||||
return commands.join(' && ');
|
||||
}
|
||||
|
||||
async function monitor() {
|
||||
let targetRunIds = [];
|
||||
if (RUN_ID_OVERRIDE) {
|
||||
targetRunIds = [RUN_ID_OVERRIDE];
|
||||
} else {
|
||||
// 1. Get runs directly associated with the branch
|
||||
const runListOutput = runGh(
|
||||
`run list --branch "${BRANCH}" --limit 10 --json databaseId,status,workflowName,createdAt`,
|
||||
);
|
||||
if (runListOutput) {
|
||||
const runs = JSON.parse(runListOutput);
|
||||
const activeRuns = runs.filter((r) => r.status !== 'completed');
|
||||
if (activeRuns.length > 0) {
|
||||
targetRunIds = activeRuns.map((r) => r.databaseId);
|
||||
} else if (runs.length > 0) {
|
||||
const latestTime = new Date(runs[0].createdAt).getTime();
|
||||
targetRunIds = runs
|
||||
.filter((r) => latestTime - new Date(r.createdAt).getTime() < 60000)
|
||||
.map((r) => r.databaseId);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Get runs associated with commit statuses (handles chained/indirect runs)
|
||||
try {
|
||||
const headSha = execSync(`git rev-parse "${BRANCH}"`).toString().trim();
|
||||
const statusOutput = runGh(
|
||||
`api repos/${REPO}/commits/${headSha}/status -q '.statuses[] | select(.target_url | contains("actions/runs/")) | .target_url'`,
|
||||
);
|
||||
if (statusOutput) {
|
||||
const statusRunIds = statusOutput
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((url) => {
|
||||
const match = url.match(/actions\/runs\/(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
for (const runId of statusRunIds) {
|
||||
if (!targetRunIds.includes(runId)) {
|
||||
targetRunIds.push(runId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore if branch/SHA not found or API fails
|
||||
}
|
||||
|
||||
if (targetRunIds.length > 0) {
|
||||
const runNames = [];
|
||||
for (const runId of targetRunIds) {
|
||||
const runInfo = runGh(`run view "${runId}" --json workflowName`);
|
||||
if (runInfo) {
|
||||
runNames.push(JSON.parse(runInfo).workflowName);
|
||||
}
|
||||
}
|
||||
console.log(`Monitoring workflows: ${[...new Set(runNames)].join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetRunIds.length === 0) {
|
||||
console.log(`No runs found for branch ${BRANCH}.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
let allPassed = 0,
|
||||
allFailed = 0,
|
||||
allRunning = 0,
|
||||
allQueued = 0,
|
||||
totalJobs = 0;
|
||||
let anyRunInProgress = false;
|
||||
const fileToTests = new Map();
|
||||
let failuresFoundInLoop = false;
|
||||
|
||||
for (const runId of targetRunIds) {
|
||||
const runOutput = runGh(
|
||||
`run view "${runId}" --json databaseId,status,conclusion,workflowName`,
|
||||
);
|
||||
if (!runOutput) continue;
|
||||
const run = JSON.parse(runOutput);
|
||||
if (run.status !== 'completed') anyRunInProgress = true;
|
||||
|
||||
const jobsOutput = runGh(`run view "${runId}" --json jobs`);
|
||||
if (jobsOutput) {
|
||||
const { jobs } = JSON.parse(jobsOutput);
|
||||
totalJobs += jobs.length;
|
||||
const failedJobs = jobs.filter((j) => j.conclusion === 'failure');
|
||||
if (failedJobs.length > 0) {
|
||||
failuresFoundInLoop = true;
|
||||
for (const job of failedJobs) {
|
||||
const failures = fetchFailuresViaApi(job.databaseId);
|
||||
if (failures.trim()) {
|
||||
failures.split('\n').forEach((line) => {
|
||||
if (!line.trim() || isNoise(line)) return;
|
||||
const file = extractTestFile(line);
|
||||
const filePath =
|
||||
file ||
|
||||
(line.toLowerCase().includes('lint')
|
||||
? 'Lint Error'
|
||||
: line.toLowerCase().includes('build')
|
||||
? 'Build Error'
|
||||
: 'Unknown File');
|
||||
let testName = line;
|
||||
if (line.includes(' > ')) {
|
||||
testName = line.split(' > ').slice(1).join(' > ').trim();
|
||||
}
|
||||
if (!fileToTests.has(filePath))
|
||||
fileToTests.set(filePath, new Set());
|
||||
fileToTests.get(filePath).add(testName);
|
||||
});
|
||||
} else {
|
||||
const step =
|
||||
job.steps?.find((s) => s.conclusion === 'failure')?.name ||
|
||||
'unknown';
|
||||
const category = step.toLowerCase().includes('lint')
|
||||
? 'Lint Error'
|
||||
: step.toLowerCase().includes('build')
|
||||
? 'Build Error'
|
||||
: 'Job Error';
|
||||
if (!fileToTests.has(category))
|
||||
fileToTests.set(category, new Set());
|
||||
fileToTests
|
||||
.get(category)
|
||||
.add(`${job.name}: Failed at step "${step}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const job of jobs) {
|
||||
if (job.status === 'in_progress') allRunning++;
|
||||
else if (job.status === 'queued') allQueued++;
|
||||
else if (job.conclusion === 'success') allPassed++;
|
||||
else if (job.conclusion === 'failure') allFailed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failuresFoundInLoop) {
|
||||
console.log(
|
||||
`\n\n❌ Failures detected across ${allFailed} job(s). Stopping monitor...`,
|
||||
);
|
||||
console.log('\n--- Structured Failure Report (Noise Filtered) ---');
|
||||
for (const [file, tests] of fileToTests.entries()) {
|
||||
console.log(`\nCategory/File: ${file}`);
|
||||
// Limit output per file if it's too large
|
||||
const testsArr = Array.from(tests).map((t) =>
|
||||
t.length > 500 ? t.substring(0, 500) + '... [TRUNCATED]' : t,
|
||||
);
|
||||
testsArr.slice(0, 10).forEach((t) => console.log(` - ${t}`));
|
||||
if (testsArr.length > 10)
|
||||
console.log(` ... and ${testsArr.length - 10} more`);
|
||||
}
|
||||
const testCmd = generateTestCommand(fileToTests);
|
||||
if (testCmd) {
|
||||
console.log('\n🚀 Run this to verify fixes:');
|
||||
console.log(testCmd);
|
||||
} else if (
|
||||
Array.from(fileToTests.keys()).some((k) => k.includes('Lint'))
|
||||
) {
|
||||
console.log('\n🚀 Run this to verify lint fixes:\nnpm run lint:all');
|
||||
}
|
||||
console.log('---------------------------------');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const completed = allPassed + allFailed;
|
||||
process.stdout.write(
|
||||
`\r⏳ Monitoring ${targetRunIds.length} runs... ${completed}/${totalJobs} jobs (${allPassed} passed, ${allFailed} failed, ${allRunning} running, ${allQueued} queued) `,
|
||||
);
|
||||
if (!anyRunInProgress) {
|
||||
console.log('\n✅ All workflows passed!');
|
||||
process.exit(0);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 15000));
|
||||
}
|
||||
}
|
||||
|
||||
monitor().catch((err) => {
|
||||
console.error('\nMonitor error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -22,6 +22,3 @@ Makefile eol=lf
|
||||
*.eot binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
|
||||
# Configure GitHub to treat Markdoc files as Markdown
|
||||
*.mdoc linguist-language=Markdown
|
||||
|
||||
@@ -175,7 +175,7 @@ runs:
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_CORE_PACKAGE_NAME}" \
|
||||
--no-tag
|
||||
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} false
|
||||
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} false --silent
|
||||
|
||||
- name: '🔗 Install latest core package'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
@@ -221,9 +221,7 @@ runs:
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_CLI_PACKAGE_NAME}" \
|
||||
--no-tag
|
||||
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
|
||||
npm dist-tag rm ${INPUTS_CLI_PACKAGE_NAME} false
|
||||
fi
|
||||
npm dist-tag rm ${INPUTS_CLI_PACKAGE_NAME} false --silent
|
||||
|
||||
- name: 'Get a2a-server Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -248,7 +246,7 @@ runs:
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
|
||||
--no-tag
|
||||
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} false
|
||||
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} false --silent
|
||||
|
||||
- name: '🔬 Verify NPM release by version'
|
||||
uses: './.github/actions/verify-release'
|
||||
|
||||
@@ -34,7 +34,7 @@ runs:
|
||||
JSON_INPUTS: '${{ toJSON(inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
with:
|
||||
ref: '${{ inputs.github-sha }}'
|
||||
fetch-depth: 0
|
||||
@@ -45,11 +45,11 @@ runs:
|
||||
shell: 'bash'
|
||||
run: 'npm run build'
|
||||
- name: 'Set up QEMU'
|
||||
uses: 'docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130' # ratchet:docker/setup-qemu-action@v3
|
||||
uses: 'docker/setup-qemu-action@v3'
|
||||
- name: 'Set up Docker Buildx'
|
||||
uses: 'docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f' # ratchet:docker/setup-buildx-action@v3
|
||||
uses: 'docker/setup-buildx-action@v3'
|
||||
- name: 'Log in to GitHub Container Registry'
|
||||
uses: 'docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9' # ratchet:docker/login-action@v3
|
||||
uses: 'docker/login-action@v3'
|
||||
with:
|
||||
registry: 'docker.io'
|
||||
username: '${{ inputs.dockerhub-username }}'
|
||||
|
||||
@@ -36,7 +36,7 @@ runs:
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
|
||||
- name: 'setup node'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Cache Linters'
|
||||
uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
|
||||
uses: 'actions/cache@v4'
|
||||
with:
|
||||
path: '${{ env.GEMINI_LINT_TEMP_DIR }}'
|
||||
key: "${{ runner.os }}-${{ runner.arch }}-linters-${{ hashFiles('scripts/lint.js') }}"
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Cache ESLint'
|
||||
uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
|
||||
uses: 'actions/cache@v4'
|
||||
with:
|
||||
path: '.eslintcache'
|
||||
key: "${{ runner.os }}-eslint-${{ hashFiles('package-lock.json', 'eslint.config.js') }}"
|
||||
@@ -114,9 +114,6 @@ jobs:
|
||||
- name: 'Run sensitive keyword linter'
|
||||
run: 'node scripts/lint.js --sensitive-keywords'
|
||||
|
||||
- name: 'Run GitHub Actions pinning linter'
|
||||
run: 'node scripts/lint.js --check-github-actions-pinning'
|
||||
|
||||
link_checker:
|
||||
name: 'Link Checker'
|
||||
runs-on: 'ubuntu-latest'
|
||||
@@ -161,12 +158,6 @@ jobs:
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Install system dependencies'
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
|
||||
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
|
||||
- name: 'Install dependencies for testing'
|
||||
run: 'npm ci'
|
||||
|
||||
|
||||
@@ -28,14 +28,14 @@ jobs:
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
uses: 'actions/create-github-app-token@v2'
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
permission-issues: 'write'
|
||||
|
||||
- name: 'Process Stale Issues'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
DRY_RUN: '${{ inputs.dry_run }}'
|
||||
with:
|
||||
|
||||
@@ -27,13 +27,13 @@ jobs:
|
||||
APP_ID: '${{ secrets.APP_ID }}'
|
||||
if: |-
|
||||
${{ env.APP_ID != '' }}
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
uses: 'actions/create-github-app-token@v2'
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
|
||||
- name: 'Process Stale PRs'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
DRY_RUN: '${{ inputs.dry_run }}'
|
||||
with:
|
||||
|
||||
@@ -18,10 +18,10 @@ jobs:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
@@ -40,10 +40,10 @@ jobs:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Check for Parent Workstream and Apply Label'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
uses: 'actions/github-script@v7'
|
||||
with:
|
||||
script: |
|
||||
const labelToAdd = 'workstream-rollup';
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
APP_ID: '${{ secrets.APP_ID }}'
|
||||
if: |-
|
||||
${{ env.APP_ID != '' }}
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
uses: 'actions/create-github-app-token@v2'
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
with:
|
||||
ref: '${{ github.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -29,14 +29,14 @@ jobs:
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
with:
|
||||
# The user-level skills need to be available to the workflow
|
||||
fetch-depth: 0
|
||||
ref: 'main'
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
- name: 'Create Pull Request'
|
||||
if: "steps.validate_version.outputs.CONTINUE == 'true'"
|
||||
uses: 'peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c' # ratchet:peter-evans/create-pull-request@v6
|
||||
uses: 'peter-evans/create-pull-request@v6'
|
||||
with:
|
||||
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
commit-message: 'docs(changelog): update for ${{ steps.release_info.outputs.VERSION }}'
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
|
||||
- name: 'Optimize Windows Performance'
|
||||
if: "matrix.os == 'windows-latest'"
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
shell: 'powershell'
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
architecture: '${{ matrix.arch }}'
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: 'Setup Windows SDK (Windows)'
|
||||
if: "matrix.os == 'windows-latest'"
|
||||
uses: 'microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce' # ratchet:microsoft/setup-msbuild@v2
|
||||
uses: 'microsoft/setup-msbuild@v2'
|
||||
|
||||
- name: 'Add Signtool to Path (Windows)'
|
||||
if: "matrix.os == 'windows-latest'"
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
npm run test:integration:sandbox:none -- --testTimeout=600000
|
||||
|
||||
- name: 'Upload Artifact'
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
uses: 'actions/upload-artifact@v4'
|
||||
with:
|
||||
name: 'gemini-cli-${{ matrix.platform_name }}'
|
||||
path: 'dist/${{ matrix.platform_name }}/'
|
||||
|
||||
@@ -40,13 +40,13 @@ jobs:
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
uses: 'actions/create-github-app-token@v2'
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
|
||||
- name: 'Unassign inactive assignees'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
DRY_RUN: '${{ inputs.dry_run }}'
|
||||
with:
|
||||
|
||||
+2
-2
@@ -323,8 +323,8 @@ fi
|
||||
|
||||
#### Formatting
|
||||
|
||||
To separately format the code in this project, run the following command from
|
||||
the root directory:
|
||||
To separately format the code in this project by running the following command
|
||||
from the root directory:
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
|
||||
@@ -30,7 +30,7 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
|
||||
## 📦 Installation
|
||||
|
||||
See
|
||||
[Gemini CLI installation, execution, and releases](https://geminicli.com/docs/get-started/installation/)
|
||||
[Gemini CLI installation, execution, and releases](./docs/get-started/installation.md)
|
||||
for recommended system specifications and a detailed installation guide.
|
||||
|
||||
### Quick Install
|
||||
|
||||
@@ -18,30 +18,6 @@ on GitHub.
|
||||
| [Preview](preview.md) | Experimental features ready for early feedback. |
|
||||
| [Stable](latest.md) | Stable, recommended for general use. |
|
||||
|
||||
## Announcements: v0.35.0 - 2026-03-24
|
||||
|
||||
- **Customizable Keyboard Shortcuts:** Users can now customize their keyboard
|
||||
shortcuts, including support for literal character keybindings and the
|
||||
extended Kitty protocol
|
||||
([#21945](https://github.com/google-gemini/gemini-cli/pull/21945),
|
||||
[#21972](https://github.com/google-gemini/gemini-cli/pull/21972) by
|
||||
@scidomino).
|
||||
- **Vim Mode Improvements:** Added missing motions (X, ~, r, f/F/t/T) and
|
||||
yank/paste support with the unnamed register
|
||||
([#21932](https://github.com/google-gemini/gemini-cli/pull/21932),
|
||||
[#22026](https://github.com/google-gemini/gemini-cli/pull/22026) by @aanari).
|
||||
- **Tool Isolation and Sandboxing:** Introduced `SandboxManager` to isolate
|
||||
process-spawning tools and added Linux bubblewrap/seccomp sandboxing support
|
||||
([#21774](https://github.com/google-gemini/gemini-cli/pull/21774),
|
||||
[#22231](https://github.com/google-gemini/gemini-cli/pull/22231) by @galz10,
|
||||
[#22680](https://github.com/google-gemini/gemini-cli/pull/22680) by
|
||||
@DavidAPierce).
|
||||
- **JIT Context Discovery:** Implemented Just-In-Time context discovery for file
|
||||
system tools to improve model performance and accuracy
|
||||
([#22082](https://github.com/google-gemini/gemini-cli/pull/22082),
|
||||
[#22736](https://github.com/google-gemini/gemini-cli/pull/22736) by
|
||||
@SandyTao520).
|
||||
|
||||
## Announcements: v0.34.0 - 2026-03-17
|
||||
|
||||
- **Plan Mode Enabled by Default:** Plan Mode is now enabled by default to help
|
||||
|
||||
+462
-361
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.35.0
|
||||
# Latest stable release: v0.34.0
|
||||
|
||||
Released: March 24, 2026
|
||||
Released: March 17, 2026
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,373 +11,474 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Customizable Keyboard Shortcuts:** Significant improvements to input
|
||||
flexibility with support for custom keybindings, literal character bindings,
|
||||
and extended terminal protocol keys.
|
||||
- **Vim Mode Enhancements:** Further refinement of the Vim modal editing
|
||||
experience, adding common motions like \`X\`, \`~\`, \`r\`, and \`f/F/t/T\`,
|
||||
along with yank and paste support.
|
||||
- **Enhanced Security through Sandboxing:** Introduction of a unified
|
||||
\`SandboxManager\` and integration of Linux-native sandboxing (bubblewrap and
|
||||
seccomp) to isolate tool execution and improve system security.
|
||||
- **JIT Context Discovery:** Improved performance and accuracy by enabling
|
||||
Just-In-Time context loading for file system tools, ensuring the model has the
|
||||
most relevant information without overwhelming the context.
|
||||
- **Subagent & Performance Updates:** Subagents are now enabled by default,
|
||||
supported by a model-driven parallel tool scheduler and code splitting for
|
||||
faster startup and more efficient task execution.
|
||||
- **Plan Mode Enabled by Default**: The comprehensive planning capability is now
|
||||
enabled by default, allowing for better structured task management and
|
||||
execution.
|
||||
- **Enhanced Sandboxing Capabilities**: Added support for native gVisor (runsc)
|
||||
sandboxing as well as experimental LXC container sandboxing to provide more
|
||||
robust and isolated execution environments.
|
||||
- **Improved Loop Detection & Recovery**: Implemented iterative loop detection
|
||||
and model feedback mechanisms to prevent the CLI from getting stuck in
|
||||
repetitive actions.
|
||||
- **Customizable UI Elements**: You can now configure a custom footer using the
|
||||
new `/footer` command, and enjoy standardized semantic focus colors for better
|
||||
history visibility.
|
||||
- **Extensive Subagent Updates**: Refinements across the tracker visualization
|
||||
tools, background process logging, and broader fallback support for models in
|
||||
tool execution scenarios.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- feat(cli): customizable keyboard shortcuts by @scidomino in
|
||||
[#21945](https://github.com/google-gemini/gemini-cli/pull/21945)
|
||||
- feat(core): Thread `AgentLoopContext` through core. by @joshualitt in
|
||||
[#21944](https://github.com/google-gemini/gemini-cli/pull/21944)
|
||||
- chore(release): bump version to 0.35.0-nightly.20260311.657f19c1f by
|
||||
- feat(cli): add chat resume footer on session quit by @lordshashank in
|
||||
[#20667](https://github.com/google-gemini/gemini-cli/pull/20667)
|
||||
- Support bold and other styles in svg snapshots by @jacob314 in
|
||||
[#20937](https://github.com/google-gemini/gemini-cli/pull/20937)
|
||||
- fix(core): increase A2A agent timeout to 30 minutes by @adamfweidman in
|
||||
[#21028](https://github.com/google-gemini/gemini-cli/pull/21028)
|
||||
- Cleanup old branches. by @jacob314 in
|
||||
[#19354](https://github.com/google-gemini/gemini-cli/pull/19354)
|
||||
- chore(release): bump version to 0.34.0-nightly.20260303.34f0c1538 by
|
||||
@gemini-cli-robot in
|
||||
[#21966](https://github.com/google-gemini/gemini-cli/pull/21966)
|
||||
- refactor(a2a): remove legacy CoreToolScheduler by @adamfweidman in
|
||||
[#21955](https://github.com/google-gemini/gemini-cli/pull/21955)
|
||||
- feat(ui): add missing vim mode motions (X, ~, r, f/F/t/T, df/dt and friends)
|
||||
by @aanari in [#21932](https://github.com/google-gemini/gemini-cli/pull/21932)
|
||||
- Feat/retry fetch notifications by @aishaneeshah in
|
||||
[#21813](https://github.com/google-gemini/gemini-cli/pull/21813)
|
||||
- fix(core): remove OAuth check from handle fallback and clean up stray file by
|
||||
@sehoon38 in [#21962](https://github.com/google-gemini/gemini-cli/pull/21962)
|
||||
- feat(cli): support literal character keybindings and extended Kitty protocol
|
||||
keys by @scidomino in
|
||||
[#21972](https://github.com/google-gemini/gemini-cli/pull/21972)
|
||||
- fix(ui): clamp cursor to last char after all NORMAL mode deletes by @aanari in
|
||||
[#21973](https://github.com/google-gemini/gemini-cli/pull/21973)
|
||||
- test(core): add missing tests for prompts/utils.ts by @krrishverma1805-web in
|
||||
[#19941](https://github.com/google-gemini/gemini-cli/pull/19941)
|
||||
- fix(cli): allow scrolling keys in copy mode (Ctrl+S selection mode) by
|
||||
@nsalerni in [#19933](https://github.com/google-gemini/gemini-cli/pull/19933)
|
||||
- docs(cli): add custom keybinding documentation by @scidomino in
|
||||
[#21980](https://github.com/google-gemini/gemini-cli/pull/21980)
|
||||
- docs: fix misleading YOLO mode description in defaultApprovalMode by
|
||||
@Gyanranjan-Priyam in
|
||||
[#21878](https://github.com/google-gemini/gemini-cli/pull/21878)
|
||||
- fix: clean up /clear and /resume by @jackwotherspoon in
|
||||
[#22007](https://github.com/google-gemini/gemini-cli/pull/22007)
|
||||
- fix(core)#20941: reap orphaned descendant processes on PTY abort by @manavmax
|
||||
in [#21124](https://github.com/google-gemini/gemini-cli/pull/21124)
|
||||
- fix(core): update language detection to use LSP 3.18 identifiers by @yunaseoul
|
||||
in [#21931](https://github.com/google-gemini/gemini-cli/pull/21931)
|
||||
- feat(cli): support removing keybindings via '-' prefix by @scidomino in
|
||||
[#22042](https://github.com/google-gemini/gemini-cli/pull/22042)
|
||||
- feat(policy): add --admin-policy flag for supplemental admin policies by
|
||||
@galz10 in [#20360](https://github.com/google-gemini/gemini-cli/pull/20360)
|
||||
- merge duplicate imports packages/cli/src subtask1 by @Nixxx19 in
|
||||
[#22040](https://github.com/google-gemini/gemini-cli/pull/22040)
|
||||
- perf(core): parallelize user quota and experiments fetching in refreshAuth by
|
||||
@sehoon38 in [#21648](https://github.com/google-gemini/gemini-cli/pull/21648)
|
||||
- Changelog for v0.34.0-preview.0 by @gemini-cli-robot in
|
||||
[#21965](https://github.com/google-gemini/gemini-cli/pull/21965)
|
||||
- Changelog for v0.33.0 by @gemini-cli-robot in
|
||||
[#21967](https://github.com/google-gemini/gemini-cli/pull/21967)
|
||||
- fix(core): handle EISDIR in robustRealpath on Windows by @sehoon38 in
|
||||
[#21984](https://github.com/google-gemini/gemini-cli/pull/21984)
|
||||
- feat(core): include initiationMethod in conversation interaction telemetry by
|
||||
@yunaseoul in [#22054](https://github.com/google-gemini/gemini-cli/pull/22054)
|
||||
- feat(ui): add vim yank/paste (y/p/P) with unnamed register by @aanari in
|
||||
[#22026](https://github.com/google-gemini/gemini-cli/pull/22026)
|
||||
- fix(core): enable numerical routing for api key users by @sehoon38 in
|
||||
[#21977](https://github.com/google-gemini/gemini-cli/pull/21977)
|
||||
- feat(telemetry): implement retry attempt telemetry for network related retries
|
||||
by @aishaneeshah in
|
||||
[#22027](https://github.com/google-gemini/gemini-cli/pull/22027)
|
||||
- fix(policy): remove unnecessary escapeRegex from pattern builders by
|
||||
@spencer426 in
|
||||
[#21921](https://github.com/google-gemini/gemini-cli/pull/21921)
|
||||
- fix(core): preserve dynamic tool descriptions on session resume by @sehoon38
|
||||
in [#18835](https://github.com/google-gemini/gemini-cli/pull/18835)
|
||||
- chore: allow 'gemini-3.1' in sensitive keyword linter by @scidomino in
|
||||
[#22065](https://github.com/google-gemini/gemini-cli/pull/22065)
|
||||
- feat(core): support custom base URL via env vars by @junaiddshaukat in
|
||||
[#21561](https://github.com/google-gemini/gemini-cli/pull/21561)
|
||||
- merge duplicate imports packages/cli/src subtask2 by @Nixxx19 in
|
||||
[#22051](https://github.com/google-gemini/gemini-cli/pull/22051)
|
||||
- fix(core): silently retry API errors up to 3 times before halting session by
|
||||
@spencer426 in
|
||||
[#21989](https://github.com/google-gemini/gemini-cli/pull/21989)
|
||||
- feat(core): simplify subagent success UI and improve early termination display
|
||||
[#21034](https://github.com/google-gemini/gemini-cli/pull/21034)
|
||||
- feat(ui): standardize semantic focus colors and enhance history visibility by
|
||||
@keithguerin in
|
||||
[#20745](https://github.com/google-gemini/gemini-cli/pull/20745)
|
||||
- fix: merge duplicate imports in packages/core (3/4) by @Nixxx19 in
|
||||
[#20928](https://github.com/google-gemini/gemini-cli/pull/20928)
|
||||
- Add extra safety checks for proto pollution by @jacob314 in
|
||||
[#20396](https://github.com/google-gemini/gemini-cli/pull/20396)
|
||||
- feat(core): Add tracker CRUD tools & visualization by @anj-s in
|
||||
[#19489](https://github.com/google-gemini/gemini-cli/pull/19489)
|
||||
- Revert "fix(ui): persist expansion in AskUser dialog when navigating options"
|
||||
by @jacob314 in
|
||||
[#21042](https://github.com/google-gemini/gemini-cli/pull/21042)
|
||||
- Changelog for v0.33.0-preview.0 by @gemini-cli-robot in
|
||||
[#21030](https://github.com/google-gemini/gemini-cli/pull/21030)
|
||||
- fix: model persistence for all scenarios by @sripasg in
|
||||
[#21051](https://github.com/google-gemini/gemini-cli/pull/21051)
|
||||
- chore/release: bump version to 0.34.0-nightly.20260304.28af4e127 by
|
||||
@gemini-cli-robot in
|
||||
[#21054](https://github.com/google-gemini/gemini-cli/pull/21054)
|
||||
- Consistently guard restarts against concurrent auto updates by @scidomino in
|
||||
[#21016](https://github.com/google-gemini/gemini-cli/pull/21016)
|
||||
- Defensive coding to reduce the risk of Maximum update depth errors by
|
||||
@jacob314 in [#20940](https://github.com/google-gemini/gemini-cli/pull/20940)
|
||||
- fix(cli): Polish shell autocomplete rendering to be a little more shell native
|
||||
feeling. by @jacob314 in
|
||||
[#20931](https://github.com/google-gemini/gemini-cli/pull/20931)
|
||||
- Docs: Update plan mode docs by @jkcinouye in
|
||||
[#19682](https://github.com/google-gemini/gemini-cli/pull/19682)
|
||||
- fix(mcp): Notifications/tools/list_changed support not working by @jacob314 in
|
||||
[#21050](https://github.com/google-gemini/gemini-cli/pull/21050)
|
||||
- fix(cli): register extension lifecycle events in DebugProfiler by
|
||||
@fayerman-source in
|
||||
[#20101](https://github.com/google-gemini/gemini-cli/pull/20101)
|
||||
- chore(dev): update vscode settings for typescriptreact by @rohit-4321 in
|
||||
[#19907](https://github.com/google-gemini/gemini-cli/pull/19907)
|
||||
- fix(cli): enable multi-arch docker builds for sandbox by @ru-aish in
|
||||
[#19821](https://github.com/google-gemini/gemini-cli/pull/19821)
|
||||
- Changelog for v0.32.0 by @gemini-cli-robot in
|
||||
[#21033](https://github.com/google-gemini/gemini-cli/pull/21033)
|
||||
- Changelog for v0.33.0-preview.1 by @gemini-cli-robot in
|
||||
[#21058](https://github.com/google-gemini/gemini-cli/pull/21058)
|
||||
- feat(core): improve @scripts/copy_files.js autocomplete to prioritize
|
||||
filenames by @sehoon38 in
|
||||
[#21064](https://github.com/google-gemini/gemini-cli/pull/21064)
|
||||
- feat(sandbox): add experimental LXC container sandbox support by @h30s in
|
||||
[#20735](https://github.com/google-gemini/gemini-cli/pull/20735)
|
||||
- feat(evals): add overall pass rate row to eval nightly summary table by
|
||||
@gundermanc in
|
||||
[#20905](https://github.com/google-gemini/gemini-cli/pull/20905)
|
||||
- feat(telemetry): include language in telemetry and fix accepted lines
|
||||
computation by @gundermanc in
|
||||
[#21126](https://github.com/google-gemini/gemini-cli/pull/21126)
|
||||
- Changelog for v0.32.1 by @gemini-cli-robot in
|
||||
[#21055](https://github.com/google-gemini/gemini-cli/pull/21055)
|
||||
- feat(core): add robustness tests, logging, and metrics for CodeAssistServer
|
||||
SSE parsing by @yunaseoul in
|
||||
[#21013](https://github.com/google-gemini/gemini-cli/pull/21013)
|
||||
- feat: add issue assignee workflow by @kartikangiras in
|
||||
[#21003](https://github.com/google-gemini/gemini-cli/pull/21003)
|
||||
- fix: improve error message when OAuth succeeds but project ID is required by
|
||||
@Nixxx19 in [#21070](https://github.com/google-gemini/gemini-cli/pull/21070)
|
||||
- feat(loop-reduction): implement iterative loop detection and model feedback by
|
||||
@aishaneeshah in
|
||||
[#20763](https://github.com/google-gemini/gemini-cli/pull/20763)
|
||||
- chore(github): require prompt approvers for agent prompt files by @gundermanc
|
||||
in [#20896](https://github.com/google-gemini/gemini-cli/pull/20896)
|
||||
- Docs: Create tools reference by @jkcinouye in
|
||||
[#19470](https://github.com/google-gemini/gemini-cli/pull/19470)
|
||||
- fix(core, a2a-server): prevent hang during OAuth in non-interactive sessions
|
||||
by @spencer426 in
|
||||
[#21045](https://github.com/google-gemini/gemini-cli/pull/21045)
|
||||
- chore(cli): enable deprecated settings removal by default by @yashodipmore in
|
||||
[#20682](https://github.com/google-gemini/gemini-cli/pull/20682)
|
||||
- feat(core): Disable fast ack helper for hints. by @joshualitt in
|
||||
[#21011](https://github.com/google-gemini/gemini-cli/pull/21011)
|
||||
- fix(ui): suppress redundant failure note when tool error note is shown by
|
||||
@NTaylorMullen in
|
||||
[#21078](https://github.com/google-gemini/gemini-cli/pull/21078)
|
||||
- docs: document planning workflows with Conductor example by @jerop in
|
||||
[#21166](https://github.com/google-gemini/gemini-cli/pull/21166)
|
||||
- feat(release): ship esbuild bundle in npm package by @genneth in
|
||||
[#19171](https://github.com/google-gemini/gemini-cli/pull/19171)
|
||||
- fix(extensions): preserve symlinks in extension source path while enforcing
|
||||
folder trust by @galz10 in
|
||||
[#20867](https://github.com/google-gemini/gemini-cli/pull/20867)
|
||||
- fix(cli): defer tool exclusions to policy engine in non-interactive mode by
|
||||
@EricRahm in [#20639](https://github.com/google-gemini/gemini-cli/pull/20639)
|
||||
- fix(ui): removed double padding on rendered content by @devr0306 in
|
||||
[#21029](https://github.com/google-gemini/gemini-cli/pull/21029)
|
||||
- fix(core): truncate excessively long lines in grep search output by
|
||||
@gundermanc in
|
||||
[#21147](https://github.com/google-gemini/gemini-cli/pull/21147)
|
||||
- feat: add custom footer configuration via `/footer` by @jackwotherspoon in
|
||||
[#19001](https://github.com/google-gemini/gemini-cli/pull/19001)
|
||||
- perf(core): fix OOM crash in long-running sessions by @WizardsForgeGames in
|
||||
[#19608](https://github.com/google-gemini/gemini-cli/pull/19608)
|
||||
- refactor(cli): categorize built-in themes into dark/ and light/ directories by
|
||||
@JayadityaGit in
|
||||
[#18634](https://github.com/google-gemini/gemini-cli/pull/18634)
|
||||
- fix(core): explicitly allow codebase_investigator and cli_help in read-only
|
||||
mode by @Adib234 in
|
||||
[#21157](https://github.com/google-gemini/gemini-cli/pull/21157)
|
||||
- test: add browser agent integration tests by @kunal-10-cloud in
|
||||
[#21151](https://github.com/google-gemini/gemini-cli/pull/21151)
|
||||
- fix(cli): fix enabling kitty codes on Windows Terminal by @scidomino in
|
||||
[#21136](https://github.com/google-gemini/gemini-cli/pull/21136)
|
||||
- refactor(core): extract shared OAuth flow primitives from MCPOAuthProvider by
|
||||
@SandyTao520 in
|
||||
[#20895](https://github.com/google-gemini/gemini-cli/pull/20895)
|
||||
- fix(ui): add partial output to cancelled shell UI by @devr0306 in
|
||||
[#21178](https://github.com/google-gemini/gemini-cli/pull/21178)
|
||||
- fix(cli): replace hardcoded keybinding strings with dynamic formatters by
|
||||
@scidomino in [#21159](https://github.com/google-gemini/gemini-cli/pull/21159)
|
||||
- DOCS: Update quota and pricing page by @g-samroberts in
|
||||
[#21194](https://github.com/google-gemini/gemini-cli/pull/21194)
|
||||
- feat(telemetry): implement Clearcut logging for startup statistics by
|
||||
@yunaseoul in [#21172](https://github.com/google-gemini/gemini-cli/pull/21172)
|
||||
- feat(triage): add area/documentation to issue triage by @g-samroberts in
|
||||
[#21222](https://github.com/google-gemini/gemini-cli/pull/21222)
|
||||
- Fix so shell calls are formatted by @jacob314 in
|
||||
[#21237](https://github.com/google-gemini/gemini-cli/pull/21237)
|
||||
- feat(cli): add native gVisor (runsc) sandboxing support by @Zheyuan-Lin in
|
||||
[#21062](https://github.com/google-gemini/gemini-cli/pull/21062)
|
||||
- docs: use absolute paths for internal links in plan-mode.md by @jerop in
|
||||
[#21299](https://github.com/google-gemini/gemini-cli/pull/21299)
|
||||
- fix(core): prevent unhandled AbortError crash during stream loop detection by
|
||||
@7hokerz in [#21123](https://github.com/google-gemini/gemini-cli/pull/21123)
|
||||
- fix:reorder env var redaction checks to scan values first by @kartikangiras in
|
||||
[#21059](https://github.com/google-gemini/gemini-cli/pull/21059)
|
||||
- fix(acp): rename --experimental-acp to --acp & remove Zed-specific refrences
|
||||
by @skeshive in
|
||||
[#21171](https://github.com/google-gemini/gemini-cli/pull/21171)
|
||||
- feat(core): fallback to 2.5 models with no access for toolcalls by @sehoon38
|
||||
in [#21283](https://github.com/google-gemini/gemini-cli/pull/21283)
|
||||
- test(core): improve testing for API request/response parsing by @sehoon38 in
|
||||
[#21227](https://github.com/google-gemini/gemini-cli/pull/21227)
|
||||
- docs(links): update docs-writer skill and fix broken link by @g-samroberts in
|
||||
[#21314](https://github.com/google-gemini/gemini-cli/pull/21314)
|
||||
- Fix code colorizer ansi escape bug. by @jacob314 in
|
||||
[#21321](https://github.com/google-gemini/gemini-cli/pull/21321)
|
||||
- remove wildcard behavior on keybindings by @scidomino in
|
||||
[#21315](https://github.com/google-gemini/gemini-cli/pull/21315)
|
||||
- feat(acp): Add support for AI Gateway auth by @skeshive in
|
||||
[#21305](https://github.com/google-gemini/gemini-cli/pull/21305)
|
||||
- fix(theme): improve theme color contrast for macOS Terminal.app by @clocky in
|
||||
[#21175](https://github.com/google-gemini/gemini-cli/pull/21175)
|
||||
- feat (core): Implement tracker related SI changes by @anj-s in
|
||||
[#19964](https://github.com/google-gemini/gemini-cli/pull/19964)
|
||||
- Changelog for v0.33.0-preview.2 by @gemini-cli-robot in
|
||||
[#21333](https://github.com/google-gemini/gemini-cli/pull/21333)
|
||||
- Changelog for v0.33.0-preview.3 by @gemini-cli-robot in
|
||||
[#21347](https://github.com/google-gemini/gemini-cli/pull/21347)
|
||||
- docs: format release times as HH:MM UTC by @pavan-sh in
|
||||
[#20726](https://github.com/google-gemini/gemini-cli/pull/20726)
|
||||
- fix(cli): implement --all flag for extensions uninstall by @sehoon38 in
|
||||
[#21319](https://github.com/google-gemini/gemini-cli/pull/21319)
|
||||
- docs: fix incorrect relative links to command reference by @kanywst in
|
||||
[#20964](https://github.com/google-gemini/gemini-cli/pull/20964)
|
||||
- documentiong ensures ripgrep by @Jatin24062005 in
|
||||
[#21298](https://github.com/google-gemini/gemini-cli/pull/21298)
|
||||
- fix(core): handle AbortError thrown during processTurn by @MumuTW in
|
||||
[#21296](https://github.com/google-gemini/gemini-cli/pull/21296)
|
||||
- docs(cli): clarify ! command output visibility in shell commands tutorial by
|
||||
@MohammedADev in
|
||||
[#21041](https://github.com/google-gemini/gemini-cli/pull/21041)
|
||||
- fix: logic for task tracker strategy and remove tracker tools by @anj-s in
|
||||
[#21355](https://github.com/google-gemini/gemini-cli/pull/21355)
|
||||
- fix(partUtils): display media type and size for inline data parts by @Aboudjem
|
||||
in [#21358](https://github.com/google-gemini/gemini-cli/pull/21358)
|
||||
- Fix(accessibility): add screen reader support to RewindViewer by @Famous077 in
|
||||
[#20750](https://github.com/google-gemini/gemini-cli/pull/20750)
|
||||
- fix(hooks): propagate stopHookActive in AfterAgent retry path (#20426) by
|
||||
@Aarchi-07 in [#20439](https://github.com/google-gemini/gemini-cli/pull/20439)
|
||||
- fix(core): deduplicate GEMINI.md files by device/inode on case-insensitive
|
||||
filesystems (#19904) by @Nixxx19 in
|
||||
[#19915](https://github.com/google-gemini/gemini-cli/pull/19915)
|
||||
- feat(core): add concurrency safety guidance for subagent delegation (#17753)
|
||||
by @abhipatel12 in
|
||||
[#21917](https://github.com/google-gemini/gemini-cli/pull/21917)
|
||||
- merge duplicate imports packages/cli/src subtask3 by @Nixxx19 in
|
||||
[#22056](https://github.com/google-gemini/gemini-cli/pull/22056)
|
||||
- fix(hooks): fix BeforeAgent/AfterAgent inconsistencies (#18514) by @krishdef7
|
||||
in [#21383](https://github.com/google-gemini/gemini-cli/pull/21383)
|
||||
- feat(core): implement SandboxManager interface and config schema by @galz10 in
|
||||
[#21774](https://github.com/google-gemini/gemini-cli/pull/21774)
|
||||
- docs: document npm deprecation warnings as safe to ignore by @h30s in
|
||||
[#20692](https://github.com/google-gemini/gemini-cli/pull/20692)
|
||||
- fix: remove status/need-triage from maintainer-only issues by @SandyTao520 in
|
||||
[#22044](https://github.com/google-gemini/gemini-cli/pull/22044)
|
||||
- fix(core): propagate subagent context to policy engine by @NTaylorMullen in
|
||||
[#22086](https://github.com/google-gemini/gemini-cli/pull/22086)
|
||||
- fix(cli): resolve skill uninstall failure when skill name is updated by
|
||||
@NTaylorMullen in
|
||||
[#22085](https://github.com/google-gemini/gemini-cli/pull/22085)
|
||||
- docs(plan): clarify interactive plan editing with Ctrl+X by @Adib234 in
|
||||
[#22076](https://github.com/google-gemini/gemini-cli/pull/22076)
|
||||
- fix(policy): ensure user policies are loaded when policyPaths is empty by
|
||||
@NTaylorMullen in
|
||||
[#22090](https://github.com/google-gemini/gemini-cli/pull/22090)
|
||||
- Docs: Add documentation for model steering (experimental). by @jkcinouye in
|
||||
[#21154](https://github.com/google-gemini/gemini-cli/pull/21154)
|
||||
- Add issue for automated changelogs by @g-samroberts in
|
||||
[#21912](https://github.com/google-gemini/gemini-cli/pull/21912)
|
||||
- fix(core): secure argsPattern and revert WEB_FETCH_TOOL_NAME escalation by
|
||||
[#21278](https://github.com/google-gemini/gemini-cli/pull/21278)
|
||||
- feat(ui): dynamically generate all keybinding hints by @scidomino in
|
||||
[#21346](https://github.com/google-gemini/gemini-cli/pull/21346)
|
||||
- feat(core): implement unified KeychainService and migrate token storage by
|
||||
@ehedlund in [#21344](https://github.com/google-gemini/gemini-cli/pull/21344)
|
||||
- fix(cli): gracefully handle --resume when no sessions exist by @SandyTao520 in
|
||||
[#21429](https://github.com/google-gemini/gemini-cli/pull/21429)
|
||||
- fix(plan): keep approved plan during chat compression by @ruomengz in
|
||||
[#21284](https://github.com/google-gemini/gemini-cli/pull/21284)
|
||||
- feat(core): implement generic CacheService and optimize setupUser by @sehoon38
|
||||
in [#21374](https://github.com/google-gemini/gemini-cli/pull/21374)
|
||||
- Update quota and pricing documentation with subscription tiers by @srithreepo
|
||||
in [#21351](https://github.com/google-gemini/gemini-cli/pull/21351)
|
||||
- fix(core): append correct OTLP paths for HTTP exporters by
|
||||
@sebastien-prudhomme in
|
||||
[#16836](https://github.com/google-gemini/gemini-cli/pull/16836)
|
||||
- Changelog for v0.33.0-preview.4 by @gemini-cli-robot in
|
||||
[#21354](https://github.com/google-gemini/gemini-cli/pull/21354)
|
||||
- feat(cli): implement dot-prefixing for slash command conflicts by @ehedlund in
|
||||
[#20979](https://github.com/google-gemini/gemini-cli/pull/20979)
|
||||
- refactor(core): standardize MCP tool naming to mcp\_ FQN format by
|
||||
@abhipatel12 in
|
||||
[#21425](https://github.com/google-gemini/gemini-cli/pull/21425)
|
||||
- feat(cli): hide gemma settings from display and mark as experimental by
|
||||
@abhipatel12 in
|
||||
[#21471](https://github.com/google-gemini/gemini-cli/pull/21471)
|
||||
- feat(skills): refine string-reviewer guidelines and description by @clocky in
|
||||
[#20368](https://github.com/google-gemini/gemini-cli/pull/20368)
|
||||
- fix(core): whitelist TERM and COLORTERM in environment sanitization by
|
||||
@deadsmash07 in
|
||||
[#20514](https://github.com/google-gemini/gemini-cli/pull/20514)
|
||||
- fix(billing): fix overage strategy lifecycle and settings integration by
|
||||
@gsquared94 in
|
||||
[#21236](https://github.com/google-gemini/gemini-cli/pull/21236)
|
||||
- fix: expand paste placeholders in TextInput on submit by @Jefftree in
|
||||
[#19946](https://github.com/google-gemini/gemini-cli/pull/19946)
|
||||
- fix(core): add in-memory cache to ChatRecordingService to prevent OOM by
|
||||
@SandyTao520 in
|
||||
[#21502](https://github.com/google-gemini/gemini-cli/pull/21502)
|
||||
- feat(cli): overhaul thinking UI by @keithguerin in
|
||||
[#18725](https://github.com/google-gemini/gemini-cli/pull/18725)
|
||||
- fix(ui): unify Ctrl+O expansion hint experience across buffer modes by
|
||||
@jwhelangoog in
|
||||
[#21474](https://github.com/google-gemini/gemini-cli/pull/21474)
|
||||
- fix(cli): correct shell height reporting by @jacob314 in
|
||||
[#21492](https://github.com/google-gemini/gemini-cli/pull/21492)
|
||||
- Make test suite pass when the GEMINI_SYSTEM_MD env variable or
|
||||
GEMINI_WRITE_SYSTEM_MD variable happens to be set locally/ by @jacob314 in
|
||||
[#21480](https://github.com/google-gemini/gemini-cli/pull/21480)
|
||||
- Disallow underspecified types by @gundermanc in
|
||||
[#21485](https://github.com/google-gemini/gemini-cli/pull/21485)
|
||||
- refactor(cli): standardize on 'reload' verb for all components by @keithguerin
|
||||
in [#20654](https://github.com/google-gemini/gemini-cli/pull/20654)
|
||||
- feat(cli): Invert quota language to 'percent used' by @keithguerin in
|
||||
[#20100](https://github.com/google-gemini/gemini-cli/pull/20100)
|
||||
- Docs: Add documentation for notifications (experimental)(macOS) by @jkcinouye
|
||||
in [#21163](https://github.com/google-gemini/gemini-cli/pull/21163)
|
||||
- Code review comments as a pr by @jacob314 in
|
||||
[#21209](https://github.com/google-gemini/gemini-cli/pull/21209)
|
||||
- feat(cli): unify /chat and /resume command UX by @LyalinDotCom in
|
||||
[#20256](https://github.com/google-gemini/gemini-cli/pull/20256)
|
||||
- docs: fix typo 'allowslisted' -> 'allowlisted' in mcp-server.md by
|
||||
@Gyanranjan-Priyam in
|
||||
[#21665](https://github.com/google-gemini/gemini-cli/pull/21665)
|
||||
- fix(core): display actual graph output in tracker_visualize tool by @anj-s in
|
||||
[#21455](https://github.com/google-gemini/gemini-cli/pull/21455)
|
||||
- fix(core): sanitize SSE-corrupted JSON and domain strings in error
|
||||
classification by @gsquared94 in
|
||||
[#21702](https://github.com/google-gemini/gemini-cli/pull/21702)
|
||||
- Docs: Make documentation links relative by @diodesign in
|
||||
[#21490](https://github.com/google-gemini/gemini-cli/pull/21490)
|
||||
- feat(cli): expose /tools desc as explicit subcommand for discoverability by
|
||||
@aworki in [#21241](https://github.com/google-gemini/gemini-cli/pull/21241)
|
||||
- feat(cli): add /compact alias for /compress command by @jackwotherspoon in
|
||||
[#21711](https://github.com/google-gemini/gemini-cli/pull/21711)
|
||||
- feat(plan): enable Plan Mode by default by @jerop in
|
||||
[#21713](https://github.com/google-gemini/gemini-cli/pull/21713)
|
||||
- feat(core): Introduce `AgentLoopContext`. by @joshualitt in
|
||||
[#21198](https://github.com/google-gemini/gemini-cli/pull/21198)
|
||||
- fix(core): resolve symlinks for non-existent paths during validation by
|
||||
@Adib234 in [#21487](https://github.com/google-gemini/gemini-cli/pull/21487)
|
||||
- docs: document tool exclusion from memory via deny policy by @Abhijit-2592 in
|
||||
[#21428](https://github.com/google-gemini/gemini-cli/pull/21428)
|
||||
- perf(core): cache loadApiKey to reduce redundant keychain access by @sehoon38
|
||||
in [#21520](https://github.com/google-gemini/gemini-cli/pull/21520)
|
||||
- feat(cli): implement /upgrade command by @sehoon38 in
|
||||
[#21511](https://github.com/google-gemini/gemini-cli/pull/21511)
|
||||
- Feat/browser agent progress emission by @kunal-10-cloud in
|
||||
[#21218](https://github.com/google-gemini/gemini-cli/pull/21218)
|
||||
- fix(settings): display objects as JSON instead of [object Object] by
|
||||
@Zheyuan-Lin in
|
||||
[#21458](https://github.com/google-gemini/gemini-cli/pull/21458)
|
||||
- Unmarshall update by @DavidAPierce in
|
||||
[#21721](https://github.com/google-gemini/gemini-cli/pull/21721)
|
||||
- Update mcp's list function to check for disablement. by @DavidAPierce in
|
||||
[#21148](https://github.com/google-gemini/gemini-cli/pull/21148)
|
||||
- robustness(core): static checks to validate history is immutable by @jacob314
|
||||
in [#21228](https://github.com/google-gemini/gemini-cli/pull/21228)
|
||||
- refactor(cli): better react patterns for BaseSettingsDialog by @psinha40898 in
|
||||
[#21206](https://github.com/google-gemini/gemini-cli/pull/21206)
|
||||
- feat(security): implement robust IP validation and safeFetch foundation by
|
||||
@alisa-alisa in
|
||||
[#21401](https://github.com/google-gemini/gemini-cli/pull/21401)
|
||||
- feat(core): improve subagent result display by @joshualitt in
|
||||
[#20378](https://github.com/google-gemini/gemini-cli/pull/20378)
|
||||
- docs: fix broken markdown syntax and anchor links in /tools by @campox747 in
|
||||
[#20902](https://github.com/google-gemini/gemini-cli/pull/20902)
|
||||
- feat(policy): support subagent-specific policies in TOML by @akh64bit in
|
||||
[#21431](https://github.com/google-gemini/gemini-cli/pull/21431)
|
||||
- Add script to speed up reviewing PRs adding a worktree. by @jacob314 in
|
||||
[#21748](https://github.com/google-gemini/gemini-cli/pull/21748)
|
||||
- fix(core): prevent infinite recursion in symlink resolution by @Adib234 in
|
||||
[#21750](https://github.com/google-gemini/gemini-cli/pull/21750)
|
||||
- fix(docs): fix headless mode docs by @ame2en in
|
||||
[#21287](https://github.com/google-gemini/gemini-cli/pull/21287)
|
||||
- feat/redesign header compact by @jacob314 in
|
||||
[#20922](https://github.com/google-gemini/gemini-cli/pull/20922)
|
||||
- refactor: migrate to useKeyMatchers hook by @scidomino in
|
||||
[#21753](https://github.com/google-gemini/gemini-cli/pull/21753)
|
||||
- perf(cli): cache loadSettings to reduce redundant disk I/O at startup by
|
||||
@sehoon38 in [#21521](https://github.com/google-gemini/gemini-cli/pull/21521)
|
||||
- fix(core): resolve Windows line ending and path separation bugs across CLI by
|
||||
@muhammadusman586 in
|
||||
[#21068](https://github.com/google-gemini/gemini-cli/pull/21068)
|
||||
- docs: fix heading formatting in commands.md and phrasing in tools-api.md by
|
||||
@campox747 in [#20679](https://github.com/google-gemini/gemini-cli/pull/20679)
|
||||
- refactor(ui): unify keybinding infrastructure and support string
|
||||
initialization by @scidomino in
|
||||
[#21776](https://github.com/google-gemini/gemini-cli/pull/21776)
|
||||
- Add support for updating extension sources and names by @chrstnb in
|
||||
[#21715](https://github.com/google-gemini/gemini-cli/pull/21715)
|
||||
- fix(core): handle GUI editor non-zero exit codes gracefully by @reyyanxahmed
|
||||
in [#20376](https://github.com/google-gemini/gemini-cli/pull/20376)
|
||||
- fix(core): destroy PTY on kill() and exception to prevent fd leak by @nbardy
|
||||
in [#21693](https://github.com/google-gemini/gemini-cli/pull/21693)
|
||||
- fix(docs): update theme screenshots and add missing themes by @ashmod in
|
||||
[#20689](https://github.com/google-gemini/gemini-cli/pull/20689)
|
||||
- refactor(cli): rename 'return' key to 'enter' internally by @scidomino in
|
||||
[#21796](https://github.com/google-gemini/gemini-cli/pull/21796)
|
||||
- build(release): restrict npm bundling to non-stable tags by @sehoon38 in
|
||||
[#21821](https://github.com/google-gemini/gemini-cli/pull/21821)
|
||||
- fix(core): override toolRegistry property for sub-agent schedulers by
|
||||
@gsquared94 in
|
||||
[#21766](https://github.com/google-gemini/gemini-cli/pull/21766)
|
||||
- fix(cli): make footer items equally spaced by @jacob314 in
|
||||
[#21843](https://github.com/google-gemini/gemini-cli/pull/21843)
|
||||
- docs: clarify global policy rules application in plan mode by @jerop in
|
||||
[#21864](https://github.com/google-gemini/gemini-cli/pull/21864)
|
||||
- fix(core): ensure correct flash model steering in plan mode implementation
|
||||
phase by @jerop in
|
||||
[#21871](https://github.com/google-gemini/gemini-cli/pull/21871)
|
||||
- fix(core): update @a2a-js/sdk to 0.3.11 by @adamfweidman in
|
||||
[#21875](https://github.com/google-gemini/gemini-cli/pull/21875)
|
||||
- refactor(core): improve API response error logging when retry by @yunaseoul in
|
||||
[#21784](https://github.com/google-gemini/gemini-cli/pull/21784)
|
||||
- fix(ui): handle headless execution in credits and upgrade dialogs by
|
||||
@gsquared94 in
|
||||
[#21850](https://github.com/google-gemini/gemini-cli/pull/21850)
|
||||
- fix(core): treat retryable errors with >5 min delay as terminal quota errors
|
||||
by @gsquared94 in
|
||||
[#21881](https://github.com/google-gemini/gemini-cli/pull/21881)
|
||||
- feat(telemetry): add specific PR, issue, and custom tracking IDs for GitHub
|
||||
Actions by @cocosheng-g in
|
||||
[#21129](https://github.com/google-gemini/gemini-cli/pull/21129)
|
||||
- feat(core): add OAuth2 Authorization Code auth provider for A2A agents by
|
||||
@SandyTao520 in
|
||||
[#21496](https://github.com/google-gemini/gemini-cli/pull/21496)
|
||||
- feat(cli): give visibility to /tools list command in the TUI and follow the
|
||||
subcommand pattern of other commands by @JayadityaGit in
|
||||
[#21213](https://github.com/google-gemini/gemini-cli/pull/21213)
|
||||
- Handle dirty worktrees better and warn about running scripts/review.sh on
|
||||
untrusted code. by @jacob314 in
|
||||
[#21791](https://github.com/google-gemini/gemini-cli/pull/21791)
|
||||
- feat(policy): support auto-add to policy by default and scoped persistence by
|
||||
@spencer426 in
|
||||
[#22104](https://github.com/google-gemini/gemini-cli/pull/22104)
|
||||
- feat(core): differentiate User-Agent for a2a-server and ACP clients by
|
||||
@bdmorgan in [#22059](https://github.com/google-gemini/gemini-cli/pull/22059)
|
||||
- refactor(core): extract ExecutionLifecycleService for tool backgrounding by
|
||||
@adamfweidman in
|
||||
[#21717](https://github.com/google-gemini/gemini-cli/pull/21717)
|
||||
- feat: Display pending and confirming tool calls by @sripasg in
|
||||
[#22106](https://github.com/google-gemini/gemini-cli/pull/22106)
|
||||
- feat(browser): implement input blocker overlay during automation by
|
||||
[#20361](https://github.com/google-gemini/gemini-cli/pull/20361)
|
||||
- fix(core): handle AbortError when ESC cancels tool execution by @PrasannaPal21
|
||||
in [#20863](https://github.com/google-gemini/gemini-cli/pull/20863)
|
||||
- fix(release): Improve Patch Release Workflow Comments: Clearer Approval
|
||||
Guidance by @jerop in
|
||||
[#21894](https://github.com/google-gemini/gemini-cli/pull/21894)
|
||||
- docs: clarify telemetry setup and comprehensive data map by @jerop in
|
||||
[#21879](https://github.com/google-gemini/gemini-cli/pull/21879)
|
||||
- feat(core): add per-model token usage to stream-json output by @yongruilin in
|
||||
[#21839](https://github.com/google-gemini/gemini-cli/pull/21839)
|
||||
- docs: remove experimental badge from plan mode in sidebar by @jerop in
|
||||
[#21906](https://github.com/google-gemini/gemini-cli/pull/21906)
|
||||
- fix(cli): prevent race condition in loop detection retry by @skyvanguard in
|
||||
[#17916](https://github.com/google-gemini/gemini-cli/pull/17916)
|
||||
- Add behavioral evals for tracker by @anj-s in
|
||||
[#20069](https://github.com/google-gemini/gemini-cli/pull/20069)
|
||||
- fix(auth): update terminology to 'sign in' and 'sign out' by @clocky in
|
||||
[#20892](https://github.com/google-gemini/gemini-cli/pull/20892)
|
||||
- docs(mcp): standardize mcp tool fqn documentation by @abhipatel12 in
|
||||
[#21664](https://github.com/google-gemini/gemini-cli/pull/21664)
|
||||
- fix(ui): prevent empty tool-group border stubs after filtering by @Aaxhirrr in
|
||||
[#21852](https://github.com/google-gemini/gemini-cli/pull/21852)
|
||||
- make command names consistent by @scidomino in
|
||||
[#21907](https://github.com/google-gemini/gemini-cli/pull/21907)
|
||||
- refactor: remove agent_card_requires_auth config flag by @adamfweidman in
|
||||
[#21914](https://github.com/google-gemini/gemini-cli/pull/21914)
|
||||
- feat(a2a): implement standardized normalization and streaming reassembly by
|
||||
@alisa-alisa in
|
||||
[#21402](https://github.com/google-gemini/gemini-cli/pull/21402)
|
||||
- feat(cli): enable skill activation via slash commands by @NTaylorMullen in
|
||||
[#21758](https://github.com/google-gemini/gemini-cli/pull/21758)
|
||||
- docs(cli): mention per-model token usage in stream-json result event by
|
||||
@yongruilin in
|
||||
[#21908](https://github.com/google-gemini/gemini-cli/pull/21908)
|
||||
- fix(plan): prevent plan truncation in approval dialog by supporting
|
||||
unconstrained heights by @Adib234 in
|
||||
[#21037](https://github.com/google-gemini/gemini-cli/pull/21037)
|
||||
- feat(a2a): switch from callback-based to event-driven tool scheduler by
|
||||
@cocosheng-g in
|
||||
[#21467](https://github.com/google-gemini/gemini-cli/pull/21467)
|
||||
- feat(voice): implement speech-friendly response formatter by @ayush31010 in
|
||||
[#20989](https://github.com/google-gemini/gemini-cli/pull/20989)
|
||||
- feat: add pulsating blue border automation overlay to browser agent by
|
||||
@kunal-10-cloud in
|
||||
[#21132](https://github.com/google-gemini/gemini-cli/pull/21132)
|
||||
- fix: register themes on extension load not start by @jackwotherspoon in
|
||||
[#22148](https://github.com/google-gemini/gemini-cli/pull/22148)
|
||||
- feat(ui): Do not show Ultra users /upgrade hint (#22154) by @sehoon38 in
|
||||
[#22156](https://github.com/google-gemini/gemini-cli/pull/22156)
|
||||
- chore: remove unnecessary log for themes by @jackwotherspoon in
|
||||
[#22165](https://github.com/google-gemini/gemini-cli/pull/22165)
|
||||
- fix(core): resolve MCP tool FQN validation, schema export, and wildcards in
|
||||
subagents by @abhipatel12 in
|
||||
[#22069](https://github.com/google-gemini/gemini-cli/pull/22069)
|
||||
- fix(cli): validate --model argument at startup by @JaisalJain in
|
||||
[#21393](https://github.com/google-gemini/gemini-cli/pull/21393)
|
||||
- fix(core): handle policy ALLOW for exit_plan_mode by @backnotprop in
|
||||
[#21802](https://github.com/google-gemini/gemini-cli/pull/21802)
|
||||
- feat(telemetry): add Clearcut instrumentation for AI credits billing events by
|
||||
@gsquared94 in
|
||||
[#22153](https://github.com/google-gemini/gemini-cli/pull/22153)
|
||||
- feat(core): add google credentials provider for remote agents by @adamfweidman
|
||||
in [#21024](https://github.com/google-gemini/gemini-cli/pull/21024)
|
||||
- test(cli): add integration test for node deprecation warnings by @Nixxx19 in
|
||||
[#20215](https://github.com/google-gemini/gemini-cli/pull/20215)
|
||||
- feat(cli): allow safe tools to execute concurrently while agent is busy by
|
||||
@spencer426 in
|
||||
[#21988](https://github.com/google-gemini/gemini-cli/pull/21988)
|
||||
- feat(core): implement model-driven parallel tool scheduler by @abhipatel12 in
|
||||
[#21933](https://github.com/google-gemini/gemini-cli/pull/21933)
|
||||
- update vulnerable deps by @scidomino in
|
||||
[#22180](https://github.com/google-gemini/gemini-cli/pull/22180)
|
||||
- fix(core): fix startup stats to use int values for timestamps and durations by
|
||||
@yunaseoul in [#22201](https://github.com/google-gemini/gemini-cli/pull/22201)
|
||||
- fix(core): prevent duplicate tool schemas for instantiated tools by
|
||||
@abhipatel12 in
|
||||
[#22204](https://github.com/google-gemini/gemini-cli/pull/22204)
|
||||
- fix(core): add proxy routing support for remote A2A subagents by @adamfweidman
|
||||
in [#22199](https://github.com/google-gemini/gemini-cli/pull/22199)
|
||||
- fix(core/ide): add Antigravity CLI fallbacks by @apfine in
|
||||
[#22030](https://github.com/google-gemini/gemini-cli/pull/22030)
|
||||
- fix(browser): fix duplicate function declaration error in browser agent by
|
||||
@gsquared94 in
|
||||
[#22207](https://github.com/google-gemini/gemini-cli/pull/22207)
|
||||
- feat(core): implement Stage 1 improvements for webfetch tool by @aishaneeshah
|
||||
in [#21313](https://github.com/google-gemini/gemini-cli/pull/21313)
|
||||
- Changelog for v0.34.0-preview.1 by @gemini-cli-robot in
|
||||
[#22194](https://github.com/google-gemini/gemini-cli/pull/22194)
|
||||
- perf(cli): enable code splitting and deferred UI loading by @sehoon38 in
|
||||
[#22117](https://github.com/google-gemini/gemini-cli/pull/22117)
|
||||
- fix: remove unused img.png from project root by @SandyTao520 in
|
||||
[#22222](https://github.com/google-gemini/gemini-cli/pull/22222)
|
||||
- docs(local model routing): add docs on how to use Gemma for local model
|
||||
routing by @douglas-reid in
|
||||
[#21365](https://github.com/google-gemini/gemini-cli/pull/21365)
|
||||
- feat(a2a): enable native gRPC support and protocol routing by @alisa-alisa in
|
||||
[#21403](https://github.com/google-gemini/gemini-cli/pull/21403)
|
||||
- fix(cli): escape @ symbols on paste to prevent unintended file expansion by
|
||||
@krishdef7 in [#21239](https://github.com/google-gemini/gemini-cli/pull/21239)
|
||||
- feat(core): add trajectoryId to ConversationOffered telemetry by @yunaseoul in
|
||||
[#22214](https://github.com/google-gemini/gemini-cli/pull/22214)
|
||||
- docs: clarify that tools.core is an allowlist for ALL built-in tools by
|
||||
@hobostay in [#18813](https://github.com/google-gemini/gemini-cli/pull/18813)
|
||||
- docs(plan): document hooks with plan mode by @ruomengz in
|
||||
[#22197](https://github.com/google-gemini/gemini-cli/pull/22197)
|
||||
- Changelog for v0.33.1 by @gemini-cli-robot in
|
||||
[#22235](https://github.com/google-gemini/gemini-cli/pull/22235)
|
||||
- build(ci): fix false positive evals trigger on merge commits by @gundermanc in
|
||||
[#22237](https://github.com/google-gemini/gemini-cli/pull/22237)
|
||||
- fix(core): explicitly pass messageBus to policy engine for MCP tool saves by
|
||||
@abhipatel12 in
|
||||
[#22255](https://github.com/google-gemini/gemini-cli/pull/22255)
|
||||
- feat(core): Fully migrate packages/core to AgentLoopContext. by @joshualitt in
|
||||
[#22115](https://github.com/google-gemini/gemini-cli/pull/22115)
|
||||
- feat(core): increase sub-agent turn and time limits by @bdmorgan in
|
||||
[#22196](https://github.com/google-gemini/gemini-cli/pull/22196)
|
||||
- feat(core): instrument file system tools for JIT context discovery by
|
||||
@SandyTao520 in
|
||||
[#22082](https://github.com/google-gemini/gemini-cli/pull/22082)
|
||||
- refactor(ui): extract pure session browser utilities by @abhipatel12 in
|
||||
[#22256](https://github.com/google-gemini/gemini-cli/pull/22256)
|
||||
- fix(plan): Fix AskUser evals by @Adib234 in
|
||||
[#22074](https://github.com/google-gemini/gemini-cli/pull/22074)
|
||||
- fix(settings): prevent j/k navigation keys from intercepting edit buffer input
|
||||
by @student-ankitpandit in
|
||||
[#21865](https://github.com/google-gemini/gemini-cli/pull/21865)
|
||||
- feat(skills): improve async-pr-review workflow and logging by @mattKorwel in
|
||||
[#21790](https://github.com/google-gemini/gemini-cli/pull/21790)
|
||||
- refactor(cli): consolidate getErrorMessage utility to core by @scidomino in
|
||||
[#22190](https://github.com/google-gemini/gemini-cli/pull/22190)
|
||||
- fix(core): show descriptive error messages when saving settings fails by
|
||||
@afarber in [#18095](https://github.com/google-gemini/gemini-cli/pull/18095)
|
||||
- docs(core): add authentication guide for remote subagents by @adamfweidman in
|
||||
[#22178](https://github.com/google-gemini/gemini-cli/pull/22178)
|
||||
- docs: overhaul subagents documentation and add /agents command by @abhipatel12
|
||||
in [#22345](https://github.com/google-gemini/gemini-cli/pull/22345)
|
||||
- refactor(ui): extract SessionBrowser static ui components by @abhipatel12 in
|
||||
[#22348](https://github.com/google-gemini/gemini-cli/pull/22348)
|
||||
- test: add Object.create context regression test and tool confirmation
|
||||
integration test by @gsquared94 in
|
||||
[#22356](https://github.com/google-gemini/gemini-cli/pull/22356)
|
||||
- feat(tracker): return TodoList display for tracker tools by @anj-s in
|
||||
[#22060](https://github.com/google-gemini/gemini-cli/pull/22060)
|
||||
- feat(agent): add allowed domain restrictions for browser agent by
|
||||
@cynthialong0-0 in
|
||||
[#21775](https://github.com/google-gemini/gemini-cli/pull/21775)
|
||||
- chore/release: bump version to 0.35.0-nightly.20260313.bb060d7a9 by
|
||||
[#21173](https://github.com/google-gemini/gemini-cli/pull/21173)
|
||||
- Add extensionRegistryURI setting to change where the registry is read from by
|
||||
@kevinjwang1 in
|
||||
[#20463](https://github.com/google-gemini/gemini-cli/pull/20463)
|
||||
- fix: patch gaxios v7 Array.toString() stream corruption by @gsquared94 in
|
||||
[#21884](https://github.com/google-gemini/gemini-cli/pull/21884)
|
||||
- fix: prevent hangs in non-interactive mode and improve agent guidance by
|
||||
@cocosheng-g in
|
||||
[#20893](https://github.com/google-gemini/gemini-cli/pull/20893)
|
||||
- Add ExtensionDetails dialog and support install by @chrstnb in
|
||||
[#20845](https://github.com/google-gemini/gemini-cli/pull/20845)
|
||||
- chore/release: bump version to 0.34.0-nightly.20260310.4653b126f by
|
||||
@gemini-cli-robot in
|
||||
[#22251](https://github.com/google-gemini/gemini-cli/pull/22251)
|
||||
- Move keychain fallback to keychain service by @chrstnb in
|
||||
[#22332](https://github.com/google-gemini/gemini-cli/pull/22332)
|
||||
- feat(core): integrate SandboxManager to sandbox all process-spawning tools by
|
||||
@galz10 in [#22231](https://github.com/google-gemini/gemini-cli/pull/22231)
|
||||
- fix(cli): support CJK input and full Unicode scalar values in terminal
|
||||
protocols by @scidomino in
|
||||
[#22353](https://github.com/google-gemini/gemini-cli/pull/22353)
|
||||
- Promote stable tests. by @gundermanc in
|
||||
[#22253](https://github.com/google-gemini/gemini-cli/pull/22253)
|
||||
- feat(tracker): add tracker policy by @anj-s in
|
||||
[#22379](https://github.com/google-gemini/gemini-cli/pull/22379)
|
||||
- feat(security): add disableAlwaysAllow setting to disable auto-approvals by
|
||||
@galz10 in [#21941](https://github.com/google-gemini/gemini-cli/pull/21941)
|
||||
- Revert "fix(cli): validate --model argument at startup" by @sehoon38 in
|
||||
[#22378](https://github.com/google-gemini/gemini-cli/pull/22378)
|
||||
- fix(mcp): handle equivalent root resource URLs in OAuth validation by @galz10
|
||||
in [#20231](https://github.com/google-gemini/gemini-cli/pull/20231)
|
||||
- fix(core): use session-specific temp directory for task tracker by @anj-s in
|
||||
[#22382](https://github.com/google-gemini/gemini-cli/pull/22382)
|
||||
- Fix issue where config was undefined. by @gundermanc in
|
||||
[#22397](https://github.com/google-gemini/gemini-cli/pull/22397)
|
||||
- fix(core): deduplicate project memory when JIT context is enabled by
|
||||
@SandyTao520 in
|
||||
[#22234](https://github.com/google-gemini/gemini-cli/pull/22234)
|
||||
- feat(prompts): implement Topic-Action-Summary model for verbosity reduction by
|
||||
@Abhijit-2592 in
|
||||
[#21503](https://github.com/google-gemini/gemini-cli/pull/21503)
|
||||
- fix(core): fix manual deletion of subagent histories by @abhipatel12 in
|
||||
[#22407](https://github.com/google-gemini/gemini-cli/pull/22407)
|
||||
- Add registry var by @kevinjwang1 in
|
||||
[#22224](https://github.com/google-gemini/gemini-cli/pull/22224)
|
||||
- Add ModelDefinitions to ModelConfigService by @kevinjwang1 in
|
||||
[#22302](https://github.com/google-gemini/gemini-cli/pull/22302)
|
||||
- fix(cli): improve command conflict handling for skills by @NTaylorMullen in
|
||||
[#21942](https://github.com/google-gemini/gemini-cli/pull/21942)
|
||||
- fix(core): merge user settings with extension-provided MCP servers by
|
||||
@abhipatel12 in
|
||||
[#22484](https://github.com/google-gemini/gemini-cli/pull/22484)
|
||||
- fix(core): skip discovery for incomplete MCP configs and resolve merge race
|
||||
condition by @abhipatel12 in
|
||||
[#22494](https://github.com/google-gemini/gemini-cli/pull/22494)
|
||||
- fix(automation): harden stale PR closer permissions and maintainer detection
|
||||
by @bdmorgan in
|
||||
[#22558](https://github.com/google-gemini/gemini-cli/pull/22558)
|
||||
- fix(automation): evaluate staleness before checking protected labels by
|
||||
@bdmorgan in [#22561](https://github.com/google-gemini/gemini-cli/pull/22561)
|
||||
- feat(agent): replace the runtime npx for browser agent chrome devtool mcp with
|
||||
pre-built bundle by @cynthialong0-0 in
|
||||
[#22213](https://github.com/google-gemini/gemini-cli/pull/22213)
|
||||
- perf: optimize TrackerService dependency checks by @anj-s in
|
||||
[#22384](https://github.com/google-gemini/gemini-cli/pull/22384)
|
||||
- docs(policy): remove trailing space from commandPrefix examples by @kawasin73
|
||||
in [#22264](https://github.com/google-gemini/gemini-cli/pull/22264)
|
||||
- fix(a2a-server): resolve unsafe assignment lint errors by @ehedlund in
|
||||
[#22661](https://github.com/google-gemini/gemini-cli/pull/22661)
|
||||
- fix: Adjust ToolGroupMessage filtering to hide Confirming and show Canceled
|
||||
tool calls. by @sripasg in
|
||||
[#22230](https://github.com/google-gemini/gemini-cli/pull/22230)
|
||||
- Disallow Object.create() and reflect. by @gundermanc in
|
||||
[#22408](https://github.com/google-gemini/gemini-cli/pull/22408)
|
||||
- Guard pro model usage by @sehoon38 in
|
||||
[#22665](https://github.com/google-gemini/gemini-cli/pull/22665)
|
||||
- refactor(core): Creates AgentSession abstraction for consolidated agent
|
||||
interface. by @mbleigh in
|
||||
[#22270](https://github.com/google-gemini/gemini-cli/pull/22270)
|
||||
- docs(changelog): remove internal commands from release notes by
|
||||
@jackwotherspoon in
|
||||
[#22529](https://github.com/google-gemini/gemini-cli/pull/22529)
|
||||
- feat: enable subagents by @abhipatel12 in
|
||||
[#22386](https://github.com/google-gemini/gemini-cli/pull/22386)
|
||||
- feat(extensions): implement cryptographic integrity verification for extension
|
||||
updates by @ehedlund in
|
||||
[#21772](https://github.com/google-gemini/gemini-cli/pull/21772)
|
||||
- feat(tracker): polish UI sorting and formatting by @anj-s in
|
||||
[#22437](https://github.com/google-gemini/gemini-cli/pull/22437)
|
||||
- Changelog for v0.34.0-preview.2 by @gemini-cli-robot in
|
||||
[#22220](https://github.com/google-gemini/gemini-cli/pull/22220)
|
||||
- fix(core): fix three JIT context bugs in read_file, read_many_files, and
|
||||
memoryDiscovery by @SandyTao520 in
|
||||
[#22679](https://github.com/google-gemini/gemini-cli/pull/22679)
|
||||
- refactor(core): introduce InjectionService with source-aware injection and
|
||||
backend-native background completions by @adamfweidman in
|
||||
[#22544](https://github.com/google-gemini/gemini-cli/pull/22544)
|
||||
- Linux sandbox bubblewrap by @DavidAPierce in
|
||||
[#22680](https://github.com/google-gemini/gemini-cli/pull/22680)
|
||||
- feat(core): increase thought signature retry resilience by @bdmorgan in
|
||||
[#22202](https://github.com/google-gemini/gemini-cli/pull/22202)
|
||||
- feat(core): implement Stage 2 security and consistency improvements for
|
||||
web_fetch by @aishaneeshah in
|
||||
[#22217](https://github.com/google-gemini/gemini-cli/pull/22217)
|
||||
- refactor(core): replace positional execute params with ExecuteOptions bag by
|
||||
@adamfweidman in
|
||||
[#22674](https://github.com/google-gemini/gemini-cli/pull/22674)
|
||||
- feat(config): enable JIT context loading by default by @SandyTao520 in
|
||||
[#22736](https://github.com/google-gemini/gemini-cli/pull/22736)
|
||||
- fix(config): ensure discoveryMaxDirs is passed to global config during
|
||||
initialization by @kevin-ramdass in
|
||||
[#22744](https://github.com/google-gemini/gemini-cli/pull/22744)
|
||||
- fix(plan): allowlist get_internal_docs in Plan Mode by @Adib234 in
|
||||
[#22668](https://github.com/google-gemini/gemini-cli/pull/22668)
|
||||
- Changelog for v0.34.0-preview.3 by @gemini-cli-robot in
|
||||
[#22393](https://github.com/google-gemini/gemini-cli/pull/22393)
|
||||
- feat(core): add foundation for subagent tool isolation by @akh64bit in
|
||||
[#22708](https://github.com/google-gemini/gemini-cli/pull/22708)
|
||||
- fix(core): handle surrogate pairs in truncateString by @sehoon38 in
|
||||
[#22754](https://github.com/google-gemini/gemini-cli/pull/22754)
|
||||
- fix(cli): override j/k navigation in settings dialog to fix search input
|
||||
conflict by @sehoon38 in
|
||||
[#22800](https://github.com/google-gemini/gemini-cli/pull/22800)
|
||||
- feat(plan): add 'All the above' option to multi-select AskUser questions by
|
||||
@Adib234 in [#22365](https://github.com/google-gemini/gemini-cli/pull/22365)
|
||||
- docs: distribute package-specific GEMINI.md context to each package by
|
||||
@SandyTao520 in
|
||||
[#22734](https://github.com/google-gemini/gemini-cli/pull/22734)
|
||||
- fix(cli): clean up stale pasted placeholder metadata after word/line deletions
|
||||
by @Jomak-x in
|
||||
[#20375](https://github.com/google-gemini/gemini-cli/pull/20375)
|
||||
- refactor(core): align JIT memory placement with tiered context model by
|
||||
@SandyTao520 in
|
||||
[#22766](https://github.com/google-gemini/gemini-cli/pull/22766)
|
||||
- Linux sandbox seccomp by @DavidAPierce in
|
||||
[#22815](https://github.com/google-gemini/gemini-cli/pull/22815)
|
||||
- fix(patch): cherry-pick 4e5dfd0 to release/v0.35.0-preview.1-pr-23074 to patch
|
||||
version v0.35.0-preview.1 and create version 0.35.0-preview.2 by
|
||||
@gemini-cli-robot in
|
||||
[#23134](https://github.com/google-gemini/gemini-cli/pull/23134)
|
||||
- fix(patch): cherry-pick daf3691 to release/v0.35.0-preview.2-pr-23558 to patch
|
||||
version v0.35.0-preview.2 and create version 0.35.0-preview.3 by
|
||||
@gemini-cli-robot in
|
||||
[#23565](https://github.com/google-gemini/gemini-cli/pull/23565)
|
||||
- fix(patch): cherry-pick b2d6dc4 to release/v0.35.0-preview.4-pr-23546
|
||||
[#21816](https://github.com/google-gemini/gemini-cli/pull/21816)
|
||||
- Changelog for v0.33.0-preview.13 by @gemini-cli-robot in
|
||||
[#21927](https://github.com/google-gemini/gemini-cli/pull/21927)
|
||||
- fix(cli): stabilize prompt layout to prevent jumping when typing by
|
||||
@NTaylorMullen in
|
||||
[#21081](https://github.com/google-gemini/gemini-cli/pull/21081)
|
||||
- fix: preserve prompt text when cancelling streaming by @Nixxx19 in
|
||||
[#21103](https://github.com/google-gemini/gemini-cli/pull/21103)
|
||||
- fix: robust UX for remote agent errors by @Shyam-Raghuwanshi in
|
||||
[#20307](https://github.com/google-gemini/gemini-cli/pull/20307)
|
||||
- feat: implement background process logging and cleanup by @galz10 in
|
||||
[#21189](https://github.com/google-gemini/gemini-cli/pull/21189)
|
||||
- Changelog for v0.33.0-preview.14 by @gemini-cli-robot in
|
||||
[#21938](https://github.com/google-gemini/gemini-cli/pull/21938)
|
||||
- fix(patch): cherry-pick 45faf4d to release/v0.34.0-preview.0-pr-22148
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
[#23585](https://github.com/google-gemini/gemini-cli/pull/23585)
|
||||
[#22174](https://github.com/google-gemini/gemini-cli/pull/22174)
|
||||
- fix(patch): cherry-pick 8432bce to release/v0.34.0-preview.1-pr-22069 to patch
|
||||
version v0.34.0-preview.1 and create version 0.34.0-preview.2 by
|
||||
@gemini-cli-robot in
|
||||
[#22205](https://github.com/google-gemini/gemini-cli/pull/22205)
|
||||
- fix(patch): cherry-pick 24adacd to release/v0.34.0-preview.2-pr-22332 to patch
|
||||
version v0.34.0-preview.2 and create version 0.34.0-preview.3 by
|
||||
@gemini-cli-robot in
|
||||
[#22391](https://github.com/google-gemini/gemini-cli/pull/22391)
|
||||
- fix(patch): cherry-pick 48130eb to release/v0.34.0-preview.3-pr-22665 to patch
|
||||
version v0.34.0-preview.3 and create version 0.34.0-preview.4 by
|
||||
@gemini-cli-robot in
|
||||
[#22719](https://github.com/google-gemini/gemini-cli/pull/22719)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.34.0...v0.35.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.33.2...v0.34.0
|
||||
|
||||
+356
-358
@@ -1,6 +1,6 @@
|
||||
# Preview release: v0.36.0-preview.3
|
||||
# Preview release: v0.35.0-preview.2
|
||||
|
||||
Released: March 25, 2026
|
||||
Released: March 19, 2026
|
||||
|
||||
Our preview release includes the latest, new, and experimental features. This
|
||||
release may not be as stable as our [latest weekly release](latest.md).
|
||||
@@ -13,370 +13,368 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Subagent Architecture Enhancements:** Significant updates to subagents,
|
||||
including local execution, tool isolation, multi-registry discovery, dynamic
|
||||
tool filtering, and JIT context injection.
|
||||
- **Enhanced Security & Sandboxing:** Implemented strict macOS sandboxing using
|
||||
Seatbelt allowlist, native Windows sandboxing, and support for
|
||||
"Write-Protected" governance files.
|
||||
- **Agent Context & State Management:** Introduced task tracker protocol
|
||||
integration, 'blocked' statuses for tasks/todos, and `AgentSession` for
|
||||
improved state management and replay semantics.
|
||||
- **Browser & ACP Capabilities:** Added privacy consent for the browser agent,
|
||||
sensitive action controls, improved API token usage metadata, and gateway auth
|
||||
support via ACP.
|
||||
- **CLI & UX Improvements:** Implemented a refreshed Composer layout, expanded
|
||||
terminal fallback warnings, dynamic model resolution, and Git worktree support
|
||||
for isolated parallel sessions.
|
||||
- **Subagents & Architecture Enhancements**: Enabled subagents and laid the
|
||||
foundation for subagent tool isolation. Added proxy routing support for remote
|
||||
A2A subagents and integrated `SandboxManager` to sandbox all process-spawning
|
||||
tools.
|
||||
- **CLI & UI Improvements**: Introduced customizable keyboard shortcuts and
|
||||
support for literal character keybindings. Added missing vim mode motions and
|
||||
CJK input support. Enabled code splitting and deferred UI loading for improved
|
||||
performance.
|
||||
- **Context & Tools Optimization**: JIT context loading is now enabled by
|
||||
default with deduplication for project memory. Introduced a model-driven
|
||||
parallel tool scheduler and allowed safe tools to execute concurrently.
|
||||
- **Security & Extensions**: Implemented cryptographic integrity verification
|
||||
for extension updates and added a `disableAlwaysAllow` setting to prevent
|
||||
auto-approvals for enhanced security.
|
||||
- **Plan Mode & Web Fetch Updates**: Added an 'All the above' option for
|
||||
multi-select AskUser questions in Plan Mode. Rolled out Stage 1 and Stage 2
|
||||
security and consistency improvements for the `web_fetch` tool.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 055ff92 to release/v0.36.0-preview.0-pr-23672 to patch
|
||||
version v0.36.0-preview.0 and create version 0.36.0-preview.1 by
|
||||
- fix(patch): cherry-pick 4e5dfd0 to release/v0.35.0-preview.1-pr-23074 to patch
|
||||
version v0.35.0-preview.1 and create version 0.35.0-preview.2 by
|
||||
@gemini-cli-robot in
|
||||
[#23723](https://github.com/google-gemini/gemini-cli/pull/23723)
|
||||
- Changelog for v0.33.2 by @gemini-cli-robot in
|
||||
[#22730](https://github.com/google-gemini/gemini-cli/pull/22730)
|
||||
- feat(core): multi-registry architecture and tool filtering for subagents by
|
||||
@akh64bit in [#22712](https://github.com/google-gemini/gemini-cli/pull/22712)
|
||||
- Changelog for v0.34.0-preview.4 by @gemini-cli-robot in
|
||||
[#22752](https://github.com/google-gemini/gemini-cli/pull/22752)
|
||||
- fix(devtools): use theme-aware text colors for console warnings and errors by
|
||||
@SandyTao520 in
|
||||
[#22181](https://github.com/google-gemini/gemini-cli/pull/22181)
|
||||
- Add support for dynamic model Resolution to ModelConfigService by @kevinjwang1
|
||||
in [#22578](https://github.com/google-gemini/gemini-cli/pull/22578)
|
||||
- chore(release): bump version to 0.36.0-nightly.20260317.2f90b4653 by
|
||||
[#23134](https://github.com/google-gemini/gemini-cli/pull/23134)
|
||||
- feat(cli): customizable keyboard shortcuts by @scidomino in
|
||||
[#21945](https://github.com/google-gemini/gemini-cli/pull/21945)
|
||||
- feat(core): Thread `AgentLoopContext` through core. by @joshualitt in
|
||||
[#21944](https://github.com/google-gemini/gemini-cli/pull/21944)
|
||||
- chore(release): bump version to 0.35.0-nightly.20260311.657f19c1f by
|
||||
@gemini-cli-robot in
|
||||
[#22858](https://github.com/google-gemini/gemini-cli/pull/22858)
|
||||
- fix(cli): use active sessionId in useLogger and improve resume robustness by
|
||||
@mattKorwel in
|
||||
[#22606](https://github.com/google-gemini/gemini-cli/pull/22606)
|
||||
- fix(cli): expand tilde in policy paths from settings.json by @abhipatel12 in
|
||||
[#22772](https://github.com/google-gemini/gemini-cli/pull/22772)
|
||||
- fix(core): add actionable warnings for terminal fallbacks (#14426) by
|
||||
[#21966](https://github.com/google-gemini/gemini-cli/pull/21966)
|
||||
- refactor(a2a): remove legacy CoreToolScheduler by @adamfweidman in
|
||||
[#21955](https://github.com/google-gemini/gemini-cli/pull/21955)
|
||||
- feat(ui): add missing vim mode motions (X, ~, r, f/F/t/T, df/dt and friends)
|
||||
by @aanari in [#21932](https://github.com/google-gemini/gemini-cli/pull/21932)
|
||||
- Feat/retry fetch notifications by @aishaneeshah in
|
||||
[#21813](https://github.com/google-gemini/gemini-cli/pull/21813)
|
||||
- fix(core): remove OAuth check from handleFallback and clean up stray file by
|
||||
@sehoon38 in [#21962](https://github.com/google-gemini/gemini-cli/pull/21962)
|
||||
- feat(cli): support literal character keybindings and extended Kitty protocol
|
||||
keys by @scidomino in
|
||||
[#21972](https://github.com/google-gemini/gemini-cli/pull/21972)
|
||||
- fix(ui): clamp cursor to last char after all NORMAL mode deletes by @aanari in
|
||||
[#21973](https://github.com/google-gemini/gemini-cli/pull/21973)
|
||||
- test(core): add missing tests for prompts/utils.ts by @krrishverma1805-web in
|
||||
[#19941](https://github.com/google-gemini/gemini-cli/pull/19941)
|
||||
- fix(cli): allow scrolling keys in copy mode (Ctrl+S selection mode) by
|
||||
@nsalerni in [#19933](https://github.com/google-gemini/gemini-cli/pull/19933)
|
||||
- docs(cli): add custom keybinding documentation by @scidomino in
|
||||
[#21980](https://github.com/google-gemini/gemini-cli/pull/21980)
|
||||
- docs: fix misleading YOLO mode description in defaultApprovalMode by
|
||||
@Gyanranjan-Priyam in
|
||||
[#21878](https://github.com/google-gemini/gemini-cli/pull/21878)
|
||||
- fix: clean up /clear and /resume by @jackwotherspoon in
|
||||
[#22007](https://github.com/google-gemini/gemini-cli/pull/22007)
|
||||
- fix(core)#20941: reap orphaned descendant processes on PTY abort by @manavmax
|
||||
in [#21124](https://github.com/google-gemini/gemini-cli/pull/21124)
|
||||
- fix(core): update language detection to use LSP 3.18 identifiers by @yunaseoul
|
||||
in [#21931](https://github.com/google-gemini/gemini-cli/pull/21931)
|
||||
- feat(cli): support removing keybindings via '-' prefix by @scidomino in
|
||||
[#22042](https://github.com/google-gemini/gemini-cli/pull/22042)
|
||||
- feat(policy): add --admin-policy flag for supplemental admin policies by
|
||||
@galz10 in [#20360](https://github.com/google-gemini/gemini-cli/pull/20360)
|
||||
- merge duplicate imports packages/cli/src subtask1 by @Nixxx19 in
|
||||
[#22040](https://github.com/google-gemini/gemini-cli/pull/22040)
|
||||
- perf(core): parallelize user quota and experiments fetching in refreshAuth by
|
||||
@sehoon38 in [#21648](https://github.com/google-gemini/gemini-cli/pull/21648)
|
||||
- Changelog for v0.34.0-preview.0 by @gemini-cli-robot in
|
||||
[#21965](https://github.com/google-gemini/gemini-cli/pull/21965)
|
||||
- Changelog for v0.33.0 by @gemini-cli-robot in
|
||||
[#21967](https://github.com/google-gemini/gemini-cli/pull/21967)
|
||||
- fix(core): handle EISDIR in robustRealpath on Windows by @sehoon38 in
|
||||
[#21984](https://github.com/google-gemini/gemini-cli/pull/21984)
|
||||
- feat(core): include initiationMethod in conversation interaction telemetry by
|
||||
@yunaseoul in [#22054](https://github.com/google-gemini/gemini-cli/pull/22054)
|
||||
- feat(ui): add vim yank/paste (y/p/P) with unnamed register by @aanari in
|
||||
[#22026](https://github.com/google-gemini/gemini-cli/pull/22026)
|
||||
- fix(core): enable numerical routing for api key users by @sehoon38 in
|
||||
[#21977](https://github.com/google-gemini/gemini-cli/pull/21977)
|
||||
- feat(telemetry): implement retry attempt telemetry for network related retries
|
||||
by @aishaneeshah in
|
||||
[#22027](https://github.com/google-gemini/gemini-cli/pull/22027)
|
||||
- fix(policy): remove unnecessary escapeRegex from pattern builders by
|
||||
@spencer426 in
|
||||
[#22211](https://github.com/google-gemini/gemini-cli/pull/22211)
|
||||
- feat(tracker): integrate task tracker protocol into core system prompt by
|
||||
@anj-s in [#22442](https://github.com/google-gemini/gemini-cli/pull/22442)
|
||||
- chore: add posttest build hooks and fix missing dependencies by @NTaylorMullen
|
||||
in [#22865](https://github.com/google-gemini/gemini-cli/pull/22865)
|
||||
- feat(a2a): add agent acknowledgment command and enhance registry discovery by
|
||||
@alisa-alisa in
|
||||
[#22389](https://github.com/google-gemini/gemini-cli/pull/22389)
|
||||
- fix(cli): automatically add all VSCode workspace folders to Gemini context by
|
||||
@sakshisemalti in
|
||||
[#21380](https://github.com/google-gemini/gemini-cli/pull/21380)
|
||||
- feat: add 'blocked' status to tasks and todos by @anj-s in
|
||||
[#22735](https://github.com/google-gemini/gemini-cli/pull/22735)
|
||||
- refactor(cli): remove extra newlines in ShellToolMessage.tsx by @NTaylorMullen
|
||||
in [#22868](https://github.com/google-gemini/gemini-cli/pull/22868)
|
||||
- fix(cli): lazily load settings in onModelChange to prevent stale closure data
|
||||
loss by @KumarADITHYA123 in
|
||||
[#20403](https://github.com/google-gemini/gemini-cli/pull/20403)
|
||||
- feat(core): subagent local execution and tool isolation by @akh64bit in
|
||||
[#22718](https://github.com/google-gemini/gemini-cli/pull/22718)
|
||||
- fix(cli): resolve subagent grouping and UI state persistence by @abhipatel12
|
||||
in [#22252](https://github.com/google-gemini/gemini-cli/pull/22252)
|
||||
- refactor(ui): extract SessionBrowser search and navigation components by
|
||||
@abhipatel12 in
|
||||
[#22377](https://github.com/google-gemini/gemini-cli/pull/22377)
|
||||
- fix: updates Docker image reference for GitHub MCP server by @jhhornn in
|
||||
[#22938](https://github.com/google-gemini/gemini-cli/pull/22938)
|
||||
- refactor(cli): group subagent trajectory deletion and use native filesystem
|
||||
testing by @abhipatel12 in
|
||||
[#22890](https://github.com/google-gemini/gemini-cli/pull/22890)
|
||||
- refactor(cli): simplify keypress and mouse providers and update tests by
|
||||
@scidomino in [#22853](https://github.com/google-gemini/gemini-cli/pull/22853)
|
||||
- Changelog for v0.34.0 by @gemini-cli-robot in
|
||||
[#22860](https://github.com/google-gemini/gemini-cli/pull/22860)
|
||||
- test(cli): simplify createMockSettings calls by @scidomino in
|
||||
[#22952](https://github.com/google-gemini/gemini-cli/pull/22952)
|
||||
- feat(ui): format multi-line banner warnings with a bold title by @keithguerin
|
||||
in [#22955](https://github.com/google-gemini/gemini-cli/pull/22955)
|
||||
- Docs: Remove references to stale Gemini CLI file structure info by
|
||||
@g-samroberts in
|
||||
[#22976](https://github.com/google-gemini/gemini-cli/pull/22976)
|
||||
- feat(ui): remove write todo list tool from UI tips by @aniruddhaadak80 in
|
||||
[#22281](https://github.com/google-gemini/gemini-cli/pull/22281)
|
||||
- Fix issue where subagent thoughts are appended. by @gundermanc in
|
||||
[#22975](https://github.com/google-gemini/gemini-cli/pull/22975)
|
||||
- Feat/browser privacy consent by @kunal-10-cloud in
|
||||
[#21119](https://github.com/google-gemini/gemini-cli/pull/21119)
|
||||
- fix(core): explicitly map execution context in LocalAgentExecutor by @akh64bit
|
||||
in [#22949](https://github.com/google-gemini/gemini-cli/pull/22949)
|
||||
- feat(plan): support plan mode in non-interactive mode by @ruomengz in
|
||||
[#22670](https://github.com/google-gemini/gemini-cli/pull/22670)
|
||||
- feat(core): implement strict macOS sandboxing using Seatbelt allowlist by
|
||||
@ehedlund in [#22832](https://github.com/google-gemini/gemini-cli/pull/22832)
|
||||
- docs: add additional notes by @abhipatel12 in
|
||||
[#23008](https://github.com/google-gemini/gemini-cli/pull/23008)
|
||||
- fix(cli): resolve duplicate footer on tool cancel via ESC (#21743) by
|
||||
@ruomengz in [#21781](https://github.com/google-gemini/gemini-cli/pull/21781)
|
||||
- Changelog for v0.35.0-preview.1 by @gemini-cli-robot in
|
||||
[#23012](https://github.com/google-gemini/gemini-cli/pull/23012)
|
||||
- fix(ui): fix flickering on small terminal heights by @devr0306 in
|
||||
[#21416](https://github.com/google-gemini/gemini-cli/pull/21416)
|
||||
- fix(acp): provide more meta in tool_call_update by @Mervap in
|
||||
[#22663](https://github.com/google-gemini/gemini-cli/pull/22663)
|
||||
- docs: add FAQ entry for checking Gemini CLI version by @surajsahani in
|
||||
[#21271](https://github.com/google-gemini/gemini-cli/pull/21271)
|
||||
- feat(core): resilient subagent tool rejection with contextual feedback by
|
||||
@abhipatel12 in
|
||||
[#22951](https://github.com/google-gemini/gemini-cli/pull/22951)
|
||||
- fix(cli): correctly handle auto-update for standalone binaries by @bdmorgan in
|
||||
[#23038](https://github.com/google-gemini/gemini-cli/pull/23038)
|
||||
- feat(core): add content-utils by @adamfweidman in
|
||||
[#22984](https://github.com/google-gemini/gemini-cli/pull/22984)
|
||||
- fix: circumvent genai sdk requirement for api key when using gateway auth via
|
||||
ACP by @sripasg in
|
||||
[#23042](https://github.com/google-gemini/gemini-cli/pull/23042)
|
||||
- fix(core): don't persist browser consent sentinel in non-interactive mode by
|
||||
@jasonmatthewsuhari in
|
||||
[#23073](https://github.com/google-gemini/gemini-cli/pull/23073)
|
||||
- fix(core): narrow browser agent description to prevent stealing URL tasks from
|
||||
web_fetch by @gsquared94 in
|
||||
[#23086](https://github.com/google-gemini/gemini-cli/pull/23086)
|
||||
- feat(cli): Partial threading of AgentLoopContext. by @joshualitt in
|
||||
[#22978](https://github.com/google-gemini/gemini-cli/pull/22978)
|
||||
- fix(browser-agent): enable "Allow all server tools" session policy by
|
||||
@cynthialong0-0 in
|
||||
[#22343](https://github.com/google-gemini/gemini-cli/pull/22343)
|
||||
- refactor(cli): integrate real config loading into async test utils by
|
||||
@scidomino in [#23040](https://github.com/google-gemini/gemini-cli/pull/23040)
|
||||
- feat(core): inject memory and JIT context into subagents by @abhipatel12 in
|
||||
[#23032](https://github.com/google-gemini/gemini-cli/pull/23032)
|
||||
- Fix logging and virtual list. by @jacob314 in
|
||||
[#23080](https://github.com/google-gemini/gemini-cli/pull/23080)
|
||||
- feat(core): cap JIT context upward traversal at git root by @SandyTao520 in
|
||||
[#23074](https://github.com/google-gemini/gemini-cli/pull/23074)
|
||||
- Docs: Minor style updates from initial docs audit. by @g-samroberts in
|
||||
[#22872](https://github.com/google-gemini/gemini-cli/pull/22872)
|
||||
- feat(core): add experimental memory manager agent to replace save_memory tool
|
||||
by @SandyTao520 in
|
||||
[#22726](https://github.com/google-gemini/gemini-cli/pull/22726)
|
||||
- Changelog for v0.35.0-preview.2 by @gemini-cli-robot in
|
||||
[#23142](https://github.com/google-gemini/gemini-cli/pull/23142)
|
||||
- Update website issue template for label and title by @g-samroberts in
|
||||
[#23036](https://github.com/google-gemini/gemini-cli/pull/23036)
|
||||
- fix: upgrade ACP SDK from 0.12 to 0.16.1 by @sripasg in
|
||||
[#23132](https://github.com/google-gemini/gemini-cli/pull/23132)
|
||||
- Update callouts to work on github. by @g-samroberts in
|
||||
[#22245](https://github.com/google-gemini/gemini-cli/pull/22245)
|
||||
- feat: ACP: Add token usage metadata to the `send` method's return value by
|
||||
@sripasg in [#23148](https://github.com/google-gemini/gemini-cli/pull/23148)
|
||||
- fix(plan): clarify that plan mode policies are combined with normal mode by
|
||||
@ruomengz in [#23158](https://github.com/google-gemini/gemini-cli/pull/23158)
|
||||
- Add ModelChain support to ModelConfigService and make ModelDialog dynamic by
|
||||
@kevinjwang1 in
|
||||
[#22914](https://github.com/google-gemini/gemini-cli/pull/22914)
|
||||
- Ensure that copied extensions are writable in the user's local directory by
|
||||
@kevinjwang1 in
|
||||
[#23016](https://github.com/google-gemini/gemini-cli/pull/23016)
|
||||
- feat(core): implement native Windows sandboxing by @mattKorwel in
|
||||
[#21807](https://github.com/google-gemini/gemini-cli/pull/21807)
|
||||
- feat(core): add support for admin-forced MCP server installations by
|
||||
@gsquared94 in
|
||||
[#23163](https://github.com/google-gemini/gemini-cli/pull/23163)
|
||||
- chore(lint): ignore .gemini directory and recursive node_modules by
|
||||
@mattKorwel in
|
||||
[#23211](https://github.com/google-gemini/gemini-cli/pull/23211)
|
||||
- feat(cli): conditionally exclude ask_user tool in ACP mode by @nmcnamara-eng
|
||||
in [#23045](https://github.com/google-gemini/gemini-cli/pull/23045)
|
||||
- feat(core): introduce AgentSession and rename stream events to agent events by
|
||||
@mbleigh in [#23159](https://github.com/google-gemini/gemini-cli/pull/23159)
|
||||
- feat(worktree): add Git worktree support for isolated parallel sessions by
|
||||
@jerop in [#22973](https://github.com/google-gemini/gemini-cli/pull/22973)
|
||||
- Add support for linking in the extension registry by @kevinjwang1 in
|
||||
[#23153](https://github.com/google-gemini/gemini-cli/pull/23153)
|
||||
- feat(extensions): add --skip-settings flag to install command by @Ratish1 in
|
||||
[#17212](https://github.com/google-gemini/gemini-cli/pull/17212)
|
||||
- feat(telemetry): track if session is running in a Git worktree by @jerop in
|
||||
[#23265](https://github.com/google-gemini/gemini-cli/pull/23265)
|
||||
- refactor(core): use absolute paths in GEMINI.md context markers by
|
||||
@SandyTao520 in
|
||||
[#23135](https://github.com/google-gemini/gemini-cli/pull/23135)
|
||||
- fix(core): add sanitization to sub agent thoughts and centralize utilities by
|
||||
@devr0306 in [#22828](https://github.com/google-gemini/gemini-cli/pull/22828)
|
||||
- feat(core): refine User-Agent for VS Code traffic (unified format) by
|
||||
@sehoon38 in [#23256](https://github.com/google-gemini/gemini-cli/pull/23256)
|
||||
- Fix schema for ModelChains by @kevinjwang1 in
|
||||
[#23284](https://github.com/google-gemini/gemini-cli/pull/23284)
|
||||
- test(cli): refactor tests for async render utilities by @scidomino in
|
||||
[#23252](https://github.com/google-gemini/gemini-cli/pull/23252)
|
||||
- feat(core): add security prompt for browser agent by @cynthialong0-0 in
|
||||
[#23241](https://github.com/google-gemini/gemini-cli/pull/23241)
|
||||
- refactor(ide): replace dynamic undici import with static fetch import by
|
||||
@cocosheng-g in
|
||||
[#23268](https://github.com/google-gemini/gemini-cli/pull/23268)
|
||||
- test(cli): address unresolved feedback from PR #23252 by @scidomino in
|
||||
[#23303](https://github.com/google-gemini/gemini-cli/pull/23303)
|
||||
- feat(browser): add sensitive action controls and read-only noise reduction by
|
||||
@cynthialong0-0 in
|
||||
[#22867](https://github.com/google-gemini/gemini-cli/pull/22867)
|
||||
- Disabling failing test while investigating by @alisa-alisa in
|
||||
[#23311](https://github.com/google-gemini/gemini-cli/pull/23311)
|
||||
- fix broken extension link in hooks guide by @Indrapal-70 in
|
||||
[#21728](https://github.com/google-gemini/gemini-cli/pull/21728)
|
||||
- fix(core): fix agent description indentation by @abhipatel12 in
|
||||
[#23315](https://github.com/google-gemini/gemini-cli/pull/23315)
|
||||
- Wrap the text under TOML rule for easier readability in policy-engine.md… by
|
||||
@CogitationOps in
|
||||
[#23076](https://github.com/google-gemini/gemini-cli/pull/23076)
|
||||
- fix(extensions): revert broken extension removal behavior by @ehedlund in
|
||||
[#23317](https://github.com/google-gemini/gemini-cli/pull/23317)
|
||||
- feat(core): set up onboarding telemetry by @yunaseoul in
|
||||
[#23118](https://github.com/google-gemini/gemini-cli/pull/23118)
|
||||
- Retry evals on API error. by @gundermanc in
|
||||
[#23322](https://github.com/google-gemini/gemini-cli/pull/23322)
|
||||
- fix(evals): remove tool restrictions and add compile-time guards by
|
||||
@SandyTao520 in
|
||||
[#23312](https://github.com/google-gemini/gemini-cli/pull/23312)
|
||||
- fix(hooks): support 'ask' decision for BeforeTool hooks by @gundermanc in
|
||||
[#21146](https://github.com/google-gemini/gemini-cli/pull/21146)
|
||||
- feat(browser): add warning message for session mode 'existing' by
|
||||
@cynthialong0-0 in
|
||||
[#23288](https://github.com/google-gemini/gemini-cli/pull/23288)
|
||||
- chore(lint): enforce zero warnings and cleanup syntax restrictions by
|
||||
@alisa-alisa in
|
||||
[#22902](https://github.com/google-gemini/gemini-cli/pull/22902)
|
||||
- fix(cli): add Esc instruction to HooksDialog footer by @abhipatel12 in
|
||||
[#23258](https://github.com/google-gemini/gemini-cli/pull/23258)
|
||||
- Disallow and suppress misused spread operator. by @gundermanc in
|
||||
[#23294](https://github.com/google-gemini/gemini-cli/pull/23294)
|
||||
- fix(core): refine CliHelpAgent description for better delegation by
|
||||
@abhipatel12 in
|
||||
[#23310](https://github.com/google-gemini/gemini-cli/pull/23310)
|
||||
- fix(core): enable global session and persistent approval for web_fetch by
|
||||
[#21921](https://github.com/google-gemini/gemini-cli/pull/21921)
|
||||
- fix(core): preserve dynamic tool descriptions on session resume by @sehoon38
|
||||
in [#18835](https://github.com/google-gemini/gemini-cli/pull/18835)
|
||||
- chore: allow 'gemini-3.1' in sensitive keyword linter by @scidomino in
|
||||
[#22065](https://github.com/google-gemini/gemini-cli/pull/22065)
|
||||
- feat(core): support custom base URL via env vars by @junaiddshaukat in
|
||||
[#21561](https://github.com/google-gemini/gemini-cli/pull/21561)
|
||||
- merge duplicate imports packages/cli/src subtask2 by @Nixxx19 in
|
||||
[#22051](https://github.com/google-gemini/gemini-cli/pull/22051)
|
||||
- fix(core): silently retry API errors up to 3 times before halting session by
|
||||
@spencer426 in
|
||||
[#21989](https://github.com/google-gemini/gemini-cli/pull/21989)
|
||||
- feat(core): simplify subagent success UI and improve early termination display
|
||||
by @abhipatel12 in
|
||||
[#21917](https://github.com/google-gemini/gemini-cli/pull/21917)
|
||||
- merge duplicate imports packages/cli/src subtask3 by @Nixxx19 in
|
||||
[#22056](https://github.com/google-gemini/gemini-cli/pull/22056)
|
||||
- fix(hooks): fix BeforeAgent/AfterAgent inconsistencies (#18514) by @krishdef7
|
||||
in [#21383](https://github.com/google-gemini/gemini-cli/pull/21383)
|
||||
- feat(core): implement SandboxManager interface and config schema by @galz10 in
|
||||
[#21774](https://github.com/google-gemini/gemini-cli/pull/21774)
|
||||
- docs: document npm deprecation warnings as safe to ignore by @h30s in
|
||||
[#20692](https://github.com/google-gemini/gemini-cli/pull/20692)
|
||||
- fix: remove status/need-triage from maintainer-only issues by @SandyTao520 in
|
||||
[#22044](https://github.com/google-gemini/gemini-cli/pull/22044)
|
||||
- fix(core): propagate subagent context to policy engine by @NTaylorMullen in
|
||||
[#22086](https://github.com/google-gemini/gemini-cli/pull/22086)
|
||||
- fix(cli): resolve skill uninstall failure when skill name is updated by
|
||||
@NTaylorMullen in
|
||||
[#23295](https://github.com/google-gemini/gemini-cli/pull/23295)
|
||||
- fix(plan): add state transition override to prevent plan mode freeze by
|
||||
@Adib234 in [#23020](https://github.com/google-gemini/gemini-cli/pull/23020)
|
||||
- fix(cli): record skill activation tool calls in chat history by @NTaylorMullen
|
||||
in [#23203](https://github.com/google-gemini/gemini-cli/pull/23203)
|
||||
- fix(core): ensure subagent tool updates apply configuration overrides
|
||||
immediately by @abhipatel12 in
|
||||
[#23161](https://github.com/google-gemini/gemini-cli/pull/23161)
|
||||
- fix(cli): resolve flicker at boundaries of list in BaseSelectionList by
|
||||
@jackwotherspoon in
|
||||
[#23298](https://github.com/google-gemini/gemini-cli/pull/23298)
|
||||
- test(cli): force generic terminal in tests to fix snapshot failures by
|
||||
@abhipatel12 in
|
||||
[#23499](https://github.com/google-gemini/gemini-cli/pull/23499)
|
||||
- Evals: PR Guidance adding workflow by @alisa-alisa in
|
||||
[#23164](https://github.com/google-gemini/gemini-cli/pull/23164)
|
||||
- feat(core): refactor SandboxManager to a stateless architecture and introduce
|
||||
explicit Deny interface by @ehedlund in
|
||||
[#23141](https://github.com/google-gemini/gemini-cli/pull/23141)
|
||||
- feat(core): add event-translator and update agent types by @adamfweidman in
|
||||
[#22985](https://github.com/google-gemini/gemini-cli/pull/22985)
|
||||
- perf(cli): parallelize and background startup cleanup tasks by @sehoon38 in
|
||||
[#23545](https://github.com/google-gemini/gemini-cli/pull/23545)
|
||||
- fix: "allow always" for commands with paths by @scidomino in
|
||||
[#23558](https://github.com/google-gemini/gemini-cli/pull/23558)
|
||||
- fix(cli): prevent terminal escape sequences from leaking on exit by
|
||||
@mattKorwel in
|
||||
[#22682](https://github.com/google-gemini/gemini-cli/pull/22682)
|
||||
- feat(cli): implement full "GEMINI CLI" logo for logged-out state by
|
||||
@keithguerin in
|
||||
[#22412](https://github.com/google-gemini/gemini-cli/pull/22412)
|
||||
- fix(plan): reserve minimum height for selection list in AskUserDialog by
|
||||
@ruomengz in [#23280](https://github.com/google-gemini/gemini-cli/pull/23280)
|
||||
- fix(core): harden AgentSession replay semantics by @adamfweidman in
|
||||
[#23548](https://github.com/google-gemini/gemini-cli/pull/23548)
|
||||
- test(core): migrate hook tests to scheduler by @abhipatel12 in
|
||||
[#23496](https://github.com/google-gemini/gemini-cli/pull/23496)
|
||||
- chore(config): disable agents by default by @abhipatel12 in
|
||||
[#23546](https://github.com/google-gemini/gemini-cli/pull/23546)
|
||||
- fix(ui): make tool confirmations take up entire terminal height by @devr0306
|
||||
in [#22366](https://github.com/google-gemini/gemini-cli/pull/22366)
|
||||
- fix(core): prevent redundant remote agent loading on model switch by
|
||||
[#22085](https://github.com/google-gemini/gemini-cli/pull/22085)
|
||||
- docs(plan): clarify interactive plan editing with Ctrl+X by @Adib234 in
|
||||
[#22076](https://github.com/google-gemini/gemini-cli/pull/22076)
|
||||
- fix(policy): ensure user policies are loaded when policyPaths is empty by
|
||||
@NTaylorMullen in
|
||||
[#22090](https://github.com/google-gemini/gemini-cli/pull/22090)
|
||||
- Docs: Add documentation for model steering (experimental). by @jkcinouye in
|
||||
[#21154](https://github.com/google-gemini/gemini-cli/pull/21154)
|
||||
- Add issue for automated changelogs by @g-samroberts in
|
||||
[#21912](https://github.com/google-gemini/gemini-cli/pull/21912)
|
||||
- fix(core): secure argsPattern and revert WEB_FETCH_TOOL_NAME escalation by
|
||||
@spencer426 in
|
||||
[#22104](https://github.com/google-gemini/gemini-cli/pull/22104)
|
||||
- feat(core): differentiate User-Agent for a2a-server and ACP clients by
|
||||
@bdmorgan in [#22059](https://github.com/google-gemini/gemini-cli/pull/22059)
|
||||
- refactor(core): extract ExecutionLifecycleService for tool backgrounding by
|
||||
@adamfweidman in
|
||||
[#23576](https://github.com/google-gemini/gemini-cli/pull/23576)
|
||||
- refactor(core): update production type imports from coreToolScheduler by
|
||||
@abhipatel12 in
|
||||
[#23498](https://github.com/google-gemini/gemini-cli/pull/23498)
|
||||
- feat(cli): always prefix extension skills with colon separator by
|
||||
@NTaylorMullen in
|
||||
[#23566](https://github.com/google-gemini/gemini-cli/pull/23566)
|
||||
- fix(core): properly support allowRedirect in policy engine by @scidomino in
|
||||
[#23579](https://github.com/google-gemini/gemini-cli/pull/23579)
|
||||
- fix(cli): prevent subcommand shadowing and skip auth for commands by
|
||||
@mattKorwel in
|
||||
[#23177](https://github.com/google-gemini/gemini-cli/pull/23177)
|
||||
- fix(test): move flaky tests to non-blocking suite by @mattKorwel in
|
||||
[#23259](https://github.com/google-gemini/gemini-cli/pull/23259)
|
||||
- Changelog for v0.35.0-preview.3 by @gemini-cli-robot in
|
||||
[#23574](https://github.com/google-gemini/gemini-cli/pull/23574)
|
||||
- feat(skills): add behavioral-evals skill with fixing and promoting guides by
|
||||
@abhipatel12 in
|
||||
[#23349](https://github.com/google-gemini/gemini-cli/pull/23349)
|
||||
- refactor(core): delete obsolete coreToolScheduler by @abhipatel12 in
|
||||
[#23502](https://github.com/google-gemini/gemini-cli/pull/23502)
|
||||
- Changelog for v0.35.0-preview.4 by @gemini-cli-robot in
|
||||
[#23581](https://github.com/google-gemini/gemini-cli/pull/23581)
|
||||
- feat(core): add LegacyAgentSession by @adamfweidman in
|
||||
[#22986](https://github.com/google-gemini/gemini-cli/pull/22986)
|
||||
- feat(test-utils): add TestMcpServerBuilder and support in TestRig by
|
||||
@abhipatel12 in
|
||||
[#23491](https://github.com/google-gemini/gemini-cli/pull/23491)
|
||||
- fix(core)!: Force policy config to specify toolName by @kschaab in
|
||||
[#23330](https://github.com/google-gemini/gemini-cli/pull/23330)
|
||||
- eval(save_memory): add multi-turn interactive evals for memoryManager by
|
||||
@SandyTao520 in
|
||||
[#23572](https://github.com/google-gemini/gemini-cli/pull/23572)
|
||||
- fix(telemetry): patch memory leak and enforce logPrompts privacy by
|
||||
[#21717](https://github.com/google-gemini/gemini-cli/pull/21717)
|
||||
- feat: Display pending and confirming tool calls by @sripasg in
|
||||
[#22106](https://github.com/google-gemini/gemini-cli/pull/22106)
|
||||
- feat(browser): implement input blocker overlay during automation by
|
||||
@kunal-10-cloud in
|
||||
[#21132](https://github.com/google-gemini/gemini-cli/pull/21132)
|
||||
- fix: register themes on extension load not start by @jackwotherspoon in
|
||||
[#22148](https://github.com/google-gemini/gemini-cli/pull/22148)
|
||||
- feat(ui): Do not show Ultra users /upgrade hint (#22154) by @sehoon38 in
|
||||
[#22156](https://github.com/google-gemini/gemini-cli/pull/22156)
|
||||
- chore: remove unnecessary log for themes by @jackwotherspoon in
|
||||
[#22165](https://github.com/google-gemini/gemini-cli/pull/22165)
|
||||
- fix(core): resolve MCP tool FQN validation, schema export, and wildcards in
|
||||
subagents by @abhipatel12 in
|
||||
[#22069](https://github.com/google-gemini/gemini-cli/pull/22069)
|
||||
- fix(cli): validate --model argument at startup by @JaisalJain in
|
||||
[#21393](https://github.com/google-gemini/gemini-cli/pull/21393)
|
||||
- fix(core): handle policy ALLOW for exit_plan_mode by @backnotprop in
|
||||
[#21802](https://github.com/google-gemini/gemini-cli/pull/21802)
|
||||
- feat(telemetry): add Clearcut instrumentation for AI credits billing events by
|
||||
@gsquared94 in
|
||||
[#22153](https://github.com/google-gemini/gemini-cli/pull/22153)
|
||||
- feat(core): add google credentials provider for remote agents by @adamfweidman
|
||||
in [#21024](https://github.com/google-gemini/gemini-cli/pull/21024)
|
||||
- test(cli): add integration test for node deprecation warnings by @Nixxx19 in
|
||||
[#20215](https://github.com/google-gemini/gemini-cli/pull/20215)
|
||||
- feat(cli): allow safe tools to execute concurrently while agent is busy by
|
||||
@spencer426 in
|
||||
[#23281](https://github.com/google-gemini/gemini-cli/pull/23281)
|
||||
- perf(cli): background IDE client to speed up initialization by @sehoon38 in
|
||||
[#23603](https://github.com/google-gemini/gemini-cli/pull/23603)
|
||||
- fix(cli): prevent Ctrl+D exit when input buffer is not empty by @wtanaka in
|
||||
[#23306](https://github.com/google-gemini/gemini-cli/pull/23306)
|
||||
- fix: ACP: separate conversational text from execute tool command title by
|
||||
@sripasg in [#23179](https://github.com/google-gemini/gemini-cli/pull/23179)
|
||||
- feat(evals): add behavioral evaluations for subagent routing by @Samee24 in
|
||||
[#23272](https://github.com/google-gemini/gemini-cli/pull/23272)
|
||||
- refactor(cli,core): foundational layout, identity management, and type safety
|
||||
by @jwhelangoog in
|
||||
[#23286](https://github.com/google-gemini/gemini-cli/pull/23286)
|
||||
- fix(core): accurately reflect subagent tool failure in UI by @abhipatel12 in
|
||||
[#23187](https://github.com/google-gemini/gemini-cli/pull/23187)
|
||||
- Changelog for v0.35.0-preview.5 by @gemini-cli-robot in
|
||||
[#23606](https://github.com/google-gemini/gemini-cli/pull/23606)
|
||||
- feat(ui): implement refreshed UX for Composer layout by @jwhelangoog in
|
||||
[#21212](https://github.com/google-gemini/gemini-cli/pull/21212)
|
||||
- fix: API key input dialog user interaction when selected Gemini API Key by
|
||||
@kartikangiras in
|
||||
[#21057](https://github.com/google-gemini/gemini-cli/pull/21057)
|
||||
- docs: update `/mcp refresh` to `/mcp reload` by @adamfweidman in
|
||||
[#23631](https://github.com/google-gemini/gemini-cli/pull/23631)
|
||||
- Implementation of sandbox "Write-Protected" Governance Files by @DavidAPierce
|
||||
in [#23139](https://github.com/google-gemini/gemini-cli/pull/23139)
|
||||
- feat(sandbox): dynamic macOS sandbox expansion and worktree support by @galz10
|
||||
in [#23301](https://github.com/google-gemini/gemini-cli/pull/23301)
|
||||
- fix(acp): Pass the cwd to `AcpFileSystemService` to avoid looping failures in
|
||||
asking for perms to write plan md file by @sripasg in
|
||||
[#23612](https://github.com/google-gemini/gemini-cli/pull/23612)
|
||||
- fix(plan): sandbox path resolution in Plan Mode to prevent hallucinations by
|
||||
@Adib234 in [#22737](https://github.com/google-gemini/gemini-cli/pull/22737)
|
||||
- feat(ui): allow immediate user input during startup by @sehoon38 in
|
||||
[#23661](https://github.com/google-gemini/gemini-cli/pull/23661)
|
||||
- refactor(sandbox): reorganize Windows sandbox files by @galz10 in
|
||||
[#23645](https://github.com/google-gemini/gemini-cli/pull/23645)
|
||||
- fix(core): improve remote agent streaming UI and UX by @adamfweidman in
|
||||
[#23633](https://github.com/google-gemini/gemini-cli/pull/23633)
|
||||
- perf(cli): optimize --version startup time by @sehoon38 in
|
||||
[#23671](https://github.com/google-gemini/gemini-cli/pull/23671)
|
||||
- refactor(core): stop gemini CLI from producing unsafe casts by @gundermanc in
|
||||
[#23611](https://github.com/google-gemini/gemini-cli/pull/23611)
|
||||
- use enableAutoUpdate in test rig by @scidomino in
|
||||
[#23681](https://github.com/google-gemini/gemini-cli/pull/23681)
|
||||
- feat(core): change user-facing auth type from oauth2 to oauth by @adamfweidman
|
||||
in [#23639](https://github.com/google-gemini/gemini-cli/pull/23639)
|
||||
- chore(deps): fix npm audit vulnerabilities by @scidomino in
|
||||
[#23679](https://github.com/google-gemini/gemini-cli/pull/23679)
|
||||
- test(evals): fix overlapping act() deadlock in app-test-helper by @Adib234 in
|
||||
[#23666](https://github.com/google-gemini/gemini-cli/pull/23666)
|
||||
[#21988](https://github.com/google-gemini/gemini-cli/pull/21988)
|
||||
- feat(core): implement model-driven parallel tool scheduler by @abhipatel12 in
|
||||
[#21933](https://github.com/google-gemini/gemini-cli/pull/21933)
|
||||
- update vulnerable deps by @scidomino in
|
||||
[#22180](https://github.com/google-gemini/gemini-cli/pull/22180)
|
||||
- fix(core): fix startup stats to use int values for timestamps and durations by
|
||||
@yunaseoul in [#22201](https://github.com/google-gemini/gemini-cli/pull/22201)
|
||||
- fix(core): prevent duplicate tool schemas for instantiated tools by
|
||||
@abhipatel12 in
|
||||
[#22204](https://github.com/google-gemini/gemini-cli/pull/22204)
|
||||
- fix(core): add proxy routing support for remote A2A subagents by @adamfweidman
|
||||
in [#22199](https://github.com/google-gemini/gemini-cli/pull/22199)
|
||||
- fix(core/ide): add Antigravity CLI fallbacks by @apfine in
|
||||
[#22030](https://github.com/google-gemini/gemini-cli/pull/22030)
|
||||
- fix(browser): fix duplicate function declaration error in browser agent by
|
||||
@gsquared94 in
|
||||
[#22207](https://github.com/google-gemini/gemini-cli/pull/22207)
|
||||
- feat(core): implement Stage 1 improvements for webfetch tool by @aishaneeshah
|
||||
in [#21313](https://github.com/google-gemini/gemini-cli/pull/21313)
|
||||
- Changelog for v0.34.0-preview.1 by @gemini-cli-robot in
|
||||
[#22194](https://github.com/google-gemini/gemini-cli/pull/22194)
|
||||
- perf(cli): enable code splitting and deferred UI loading by @sehoon38 in
|
||||
[#22117](https://github.com/google-gemini/gemini-cli/pull/22117)
|
||||
- fix: remove unused img.png from project root by @SandyTao520 in
|
||||
[#22222](https://github.com/google-gemini/gemini-cli/pull/22222)
|
||||
- docs(local model routing): add docs on how to use Gemma for local model
|
||||
routing by @douglas-reid in
|
||||
[#21365](https://github.com/google-gemini/gemini-cli/pull/21365)
|
||||
- feat(a2a): enable native gRPC support and protocol routing by @alisa-alisa in
|
||||
[#21403](https://github.com/google-gemini/gemini-cli/pull/21403)
|
||||
- fix(cli): escape @ symbols on paste to prevent unintended file expansion by
|
||||
@krishdef7 in [#21239](https://github.com/google-gemini/gemini-cli/pull/21239)
|
||||
- feat(core): add trajectoryId to ConversationOffered telemetry by @yunaseoul in
|
||||
[#22214](https://github.com/google-gemini/gemini-cli/pull/22214)
|
||||
- docs: clarify that tools.core is an allowlist for ALL built-in tools by
|
||||
@hobostay in [#18813](https://github.com/google-gemini/gemini-cli/pull/18813)
|
||||
- docs(plan): document hooks with plan mode by @ruomengz in
|
||||
[#22197](https://github.com/google-gemini/gemini-cli/pull/22197)
|
||||
- Changelog for v0.33.1 by @gemini-cli-robot in
|
||||
[#22235](https://github.com/google-gemini/gemini-cli/pull/22235)
|
||||
- build(ci): fix false positive evals trigger on merge commits by @gundermanc in
|
||||
[#22237](https://github.com/google-gemini/gemini-cli/pull/22237)
|
||||
- fix(core): explicitly pass messageBus to policy engine for MCP tool saves by
|
||||
@abhipatel12 in
|
||||
[#22255](https://github.com/google-gemini/gemini-cli/pull/22255)
|
||||
- feat(core): Fully migrate packages/core to AgentLoopContext. by @joshualitt in
|
||||
[#22115](https://github.com/google-gemini/gemini-cli/pull/22115)
|
||||
- feat(core): increase sub-agent turn and time limits by @bdmorgan in
|
||||
[#22196](https://github.com/google-gemini/gemini-cli/pull/22196)
|
||||
- feat(core): instrument file system tools for JIT context discovery by
|
||||
@SandyTao520 in
|
||||
[#22082](https://github.com/google-gemini/gemini-cli/pull/22082)
|
||||
- refactor(ui): extract pure session browser utilities by @abhipatel12 in
|
||||
[#22256](https://github.com/google-gemini/gemini-cli/pull/22256)
|
||||
- fix(plan): Fix AskUser evals by @Adib234 in
|
||||
[#22074](https://github.com/google-gemini/gemini-cli/pull/22074)
|
||||
- fix(settings): prevent j/k navigation keys from intercepting edit buffer input
|
||||
by @student-ankitpandit in
|
||||
[#21865](https://github.com/google-gemini/gemini-cli/pull/21865)
|
||||
- feat(skills): improve async-pr-review workflow and logging by @mattKorwel in
|
||||
[#21790](https://github.com/google-gemini/gemini-cli/pull/21790)
|
||||
- refactor(cli): consolidate getErrorMessage utility to core by @scidomino in
|
||||
[#22190](https://github.com/google-gemini/gemini-cli/pull/22190)
|
||||
- fix(core): show descriptive error messages when saving settings fails by
|
||||
@afarber in [#18095](https://github.com/google-gemini/gemini-cli/pull/18095)
|
||||
- docs(core): add authentication guide for remote subagents by @adamfweidman in
|
||||
[#22178](https://github.com/google-gemini/gemini-cli/pull/22178)
|
||||
- docs: overhaul subagents documentation and add /agents command by @abhipatel12
|
||||
in [#22345](https://github.com/google-gemini/gemini-cli/pull/22345)
|
||||
- refactor(ui): extract SessionBrowser static ui components by @abhipatel12 in
|
||||
[#22348](https://github.com/google-gemini/gemini-cli/pull/22348)
|
||||
- test: add Object.create context regression test and tool confirmation
|
||||
integration test by @gsquared94 in
|
||||
[#22356](https://github.com/google-gemini/gemini-cli/pull/22356)
|
||||
- feat(tracker): return TodoList display for tracker tools by @anj-s in
|
||||
[#22060](https://github.com/google-gemini/gemini-cli/pull/22060)
|
||||
- feat(agent): add allowed domain restrictions for browser agent by
|
||||
@cynthialong0-0 in
|
||||
[#21775](https://github.com/google-gemini/gemini-cli/pull/21775)
|
||||
- chore/release: bump version to 0.35.0-nightly.20260313.bb060d7a9 by
|
||||
@gemini-cli-robot in
|
||||
[#22251](https://github.com/google-gemini/gemini-cli/pull/22251)
|
||||
- Move keychain fallback to keychain service by @chrstnb in
|
||||
[#22332](https://github.com/google-gemini/gemini-cli/pull/22332)
|
||||
- feat(core): integrate SandboxManager to sandbox all process-spawning tools by
|
||||
@galz10 in [#22231](https://github.com/google-gemini/gemini-cli/pull/22231)
|
||||
- fix(cli): support CJK input and full Unicode scalar values in terminal
|
||||
protocols by @scidomino in
|
||||
[#22353](https://github.com/google-gemini/gemini-cli/pull/22353)
|
||||
- Promote stable tests. by @gundermanc in
|
||||
[#22253](https://github.com/google-gemini/gemini-cli/pull/22253)
|
||||
- feat(tracker): add tracker policy by @anj-s in
|
||||
[#22379](https://github.com/google-gemini/gemini-cli/pull/22379)
|
||||
- feat(security): add disableAlwaysAllow setting to disable auto-approvals by
|
||||
@galz10 in [#21941](https://github.com/google-gemini/gemini-cli/pull/21941)
|
||||
- Revert "fix(cli): validate --model argument at startup" by @sehoon38 in
|
||||
[#22378](https://github.com/google-gemini/gemini-cli/pull/22378)
|
||||
- fix(mcp): handle equivalent root resource URLs in OAuth validation by @galz10
|
||||
in [#20231](https://github.com/google-gemini/gemini-cli/pull/20231)
|
||||
- fix(core): use session-specific temp directory for task tracker by @anj-s in
|
||||
[#22382](https://github.com/google-gemini/gemini-cli/pull/22382)
|
||||
- Fix issue where config was undefined. by @gundermanc in
|
||||
[#22397](https://github.com/google-gemini/gemini-cli/pull/22397)
|
||||
- fix(core): deduplicate project memory when JIT context is enabled by
|
||||
@SandyTao520 in
|
||||
[#22234](https://github.com/google-gemini/gemini-cli/pull/22234)
|
||||
- feat(prompts): implement Topic-Action-Summary model for verbosity reduction by
|
||||
@Abhijit-2592 in
|
||||
[#21503](https://github.com/google-gemini/gemini-cli/pull/21503)
|
||||
- fix(core): fix manual deletion of subagent histories by @abhipatel12 in
|
||||
[#22407](https://github.com/google-gemini/gemini-cli/pull/22407)
|
||||
- Add registry var by @kevinjwang1 in
|
||||
[#22224](https://github.com/google-gemini/gemini-cli/pull/22224)
|
||||
- Add ModelDefinitions to ModelConfigService by @kevinjwang1 in
|
||||
[#22302](https://github.com/google-gemini/gemini-cli/pull/22302)
|
||||
- fix(cli): improve command conflict handling for skills by @NTaylorMullen in
|
||||
[#21942](https://github.com/google-gemini/gemini-cli/pull/21942)
|
||||
- fix(core): merge user settings with extension-provided MCP servers by
|
||||
@abhipatel12 in
|
||||
[#22484](https://github.com/google-gemini/gemini-cli/pull/22484)
|
||||
- fix(core): skip discovery for incomplete MCP configs and resolve merge race
|
||||
condition by @abhipatel12 in
|
||||
[#22494](https://github.com/google-gemini/gemini-cli/pull/22494)
|
||||
- fix(automation): harden stale PR closer permissions and maintainer detection
|
||||
by @bdmorgan in
|
||||
[#22558](https://github.com/google-gemini/gemini-cli/pull/22558)
|
||||
- fix(automation): evaluate staleness before checking protected labels by
|
||||
@bdmorgan in [#22561](https://github.com/google-gemini/gemini-cli/pull/22561)
|
||||
- feat(agent): replace the runtime npx for browser agent chrome devtool mcp with
|
||||
pre-built bundle by @cynthialong0-0 in
|
||||
[#22213](https://github.com/google-gemini/gemini-cli/pull/22213)
|
||||
- perf: optimize TrackerService dependency checks by @anj-s in
|
||||
[#22384](https://github.com/google-gemini/gemini-cli/pull/22384)
|
||||
- docs(policy): remove trailing space from commandPrefix examples by @kawasin73
|
||||
in [#22264](https://github.com/google-gemini/gemini-cli/pull/22264)
|
||||
- fix(a2a-server): resolve unsafe assignment lint errors by @ehedlund in
|
||||
[#22661](https://github.com/google-gemini/gemini-cli/pull/22661)
|
||||
- fix: Adjust ToolGroupMessage filtering to hide Confirming and show Canceled
|
||||
tool calls. by @sripasg in
|
||||
[#22230](https://github.com/google-gemini/gemini-cli/pull/22230)
|
||||
- Disallow Object.create() and reflect. by @gundermanc in
|
||||
[#22408](https://github.com/google-gemini/gemini-cli/pull/22408)
|
||||
- Guard pro model usage by @sehoon38 in
|
||||
[#22665](https://github.com/google-gemini/gemini-cli/pull/22665)
|
||||
- refactor(core): Creates AgentSession abstraction for consolidated agent
|
||||
interface. by @mbleigh in
|
||||
[#22270](https://github.com/google-gemini/gemini-cli/pull/22270)
|
||||
- docs(changelog): remove internal commands from release notes by
|
||||
@jackwotherspoon in
|
||||
[#22529](https://github.com/google-gemini/gemini-cli/pull/22529)
|
||||
- feat: enable subagents by @abhipatel12 in
|
||||
[#22386](https://github.com/google-gemini/gemini-cli/pull/22386)
|
||||
- feat(extensions): implement cryptographic integrity verification for extension
|
||||
updates by @ehedlund in
|
||||
[#21772](https://github.com/google-gemini/gemini-cli/pull/21772)
|
||||
- feat(tracker): polish UI sorting and formatting by @anj-s in
|
||||
[#22437](https://github.com/google-gemini/gemini-cli/pull/22437)
|
||||
- Changelog for v0.34.0-preview.2 by @gemini-cli-robot in
|
||||
[#22220](https://github.com/google-gemini/gemini-cli/pull/22220)
|
||||
- fix(core): fix three JIT context bugs in read_file, read_many_files, and
|
||||
memoryDiscovery by @SandyTao520 in
|
||||
[#22679](https://github.com/google-gemini/gemini-cli/pull/22679)
|
||||
- refactor(core): introduce InjectionService with source-aware injection and
|
||||
backend-native background completions by @adamfweidman in
|
||||
[#22544](https://github.com/google-gemini/gemini-cli/pull/22544)
|
||||
- Linux sandbox bubblewrap by @DavidAPierce in
|
||||
[#22680](https://github.com/google-gemini/gemini-cli/pull/22680)
|
||||
- feat(core): increase thought signature retry resilience by @bdmorgan in
|
||||
[#22202](https://github.com/google-gemini/gemini-cli/pull/22202)
|
||||
- feat(core): implement Stage 2 security and consistency improvements for
|
||||
web_fetch by @aishaneeshah in
|
||||
[#22217](https://github.com/google-gemini/gemini-cli/pull/22217)
|
||||
- refactor(core): replace positional execute params with ExecuteOptions bag by
|
||||
@adamfweidman in
|
||||
[#22674](https://github.com/google-gemini/gemini-cli/pull/22674)
|
||||
- feat(config): enable JIT context loading by default by @SandyTao520 in
|
||||
[#22736](https://github.com/google-gemini/gemini-cli/pull/22736)
|
||||
- fix(config): ensure discoveryMaxDirs is passed to global config during
|
||||
initialization by @kevin-ramdass in
|
||||
[#22744](https://github.com/google-gemini/gemini-cli/pull/22744)
|
||||
- fix(plan): allowlist get_internal_docs in Plan Mode by @Adib234 in
|
||||
[#22668](https://github.com/google-gemini/gemini-cli/pull/22668)
|
||||
- Changelog for v0.34.0-preview.3 by @gemini-cli-robot in
|
||||
[#22393](https://github.com/google-gemini/gemini-cli/pull/22393)
|
||||
- feat(core): add foundation for subagent tool isolation by @akh64bit in
|
||||
[#22708](https://github.com/google-gemini/gemini-cli/pull/22708)
|
||||
- fix(core): handle surrogate pairs in truncateString by @sehoon38 in
|
||||
[#22754](https://github.com/google-gemini/gemini-cli/pull/22754)
|
||||
- fix(cli): override j/k navigation in settings dialog to fix search input
|
||||
conflict by @sehoon38 in
|
||||
[#22800](https://github.com/google-gemini/gemini-cli/pull/22800)
|
||||
- feat(plan): add 'All the above' option to multi-select AskUser questions by
|
||||
@Adib234 in [#22365](https://github.com/google-gemini/gemini-cli/pull/22365)
|
||||
- docs: distribute package-specific GEMINI.md context to each package by
|
||||
@SandyTao520 in
|
||||
[#22734](https://github.com/google-gemini/gemini-cli/pull/22734)
|
||||
- fix(cli): clean up stale pasted placeholder metadata after word/line deletions
|
||||
by @Jomak-x in
|
||||
[#20375](https://github.com/google-gemini/gemini-cli/pull/20375)
|
||||
- refactor(core): align JIT memory placement with tiered context model by
|
||||
@SandyTao520 in
|
||||
[#22766](https://github.com/google-gemini/gemini-cli/pull/22766)
|
||||
- Linux sandbox seccomp by @DavidAPierce in
|
||||
[#22815](https://github.com/google-gemini/gemini-cli/pull/22815)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.35.0-preview.5...v0.36.0-preview.3
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.34.0-preview.4...v0.35.0-preview.2
|
||||
|
||||
@@ -200,7 +200,6 @@ your specific environment.
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = "*"
|
||||
mcpName = "*"
|
||||
toolAnnotations = { readOnlyHint = true }
|
||||
decision = "allow"
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ To set up runsc:
|
||||
2. Configure the Docker daemon to use the runsc runtime.
|
||||
3. Verify the installation.
|
||||
|
||||
### 5. LXC/LXD (Linux only, experimental)
|
||||
### 4. 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
|
||||
|
||||
@@ -30,7 +30,7 @@ they appear in the UI.
|
||||
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` |
|
||||
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
|
||||
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
|
||||
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` |
|
||||
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
|
||||
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
|
||||
| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` |
|
||||
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
|
||||
|
||||
+11
-101
@@ -51,13 +51,12 @@ You can place them in:
|
||||
|
||||
### Configuration schema
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :---------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------- |
|
||||
| `kind` | string | Yes | Must be `remote`. |
|
||||
| `name` | string | Yes | A unique name for the agent. Must be a valid slug (lowercase letters, numbers, hyphens, and underscores only). |
|
||||
| `agent_card_url` | string | Yes\* | The URL to the agent's A2A card endpoint. Required if `agent_card_json` is not provided. |
|
||||
| `agent_card_json` | string | Yes\* | The inline JSON string of the agent's A2A card. Required if `agent_card_url` is not provided. |
|
||||
| `auth` | object | No | Authentication configuration. See [Authentication](#authentication). |
|
||||
| Field | Type | Required | Description |
|
||||
| :--------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------- |
|
||||
| `kind` | string | Yes | Must be `remote`. |
|
||||
| `name` | string | Yes | A unique name for the agent. Must be a valid slug (lowercase letters, numbers, hyphens, and underscores only). |
|
||||
| `agent_card_url` | string | Yes | The URL to the agent's A2A card endpoint. |
|
||||
| `auth` | object | No | Authentication configuration. See [Authentication](#authentication). |
|
||||
|
||||
### Single-subagent example
|
||||
|
||||
@@ -89,95 +88,6 @@ Markdown file.
|
||||
> [!NOTE] Mixed local and remote agents, or multiple local agents, are not
|
||||
> supported in a single file; the list format is currently remote-only.
|
||||
|
||||
### Inline Agent Card JSON
|
||||
|
||||
<details>
|
||||
<summary>View formatting options for JSON strings</summary>
|
||||
|
||||
If you don't have an endpoint serving the agent card, you can provide the A2A
|
||||
card directly as a JSON string using `agent_card_json`.
|
||||
|
||||
When providing a JSON string in YAML, you must properly format it as a string
|
||||
scalar. You can use single quotes, a block scalar, or double quotes (which
|
||||
require escaping internal double quotes).
|
||||
|
||||
#### Using single quotes
|
||||
|
||||
Single quotes allow you to embed unescaped double quotes inside the JSON string.
|
||||
This format is useful for shorter, single-line JSON strings.
|
||||
|
||||
```markdown
|
||||
---
|
||||
kind: remote
|
||||
name: single-quotes-agent
|
||||
agent_card_json:
|
||||
'{ "protocolVersion": "0.3.0", "name": "Example Agent", "version": "1.0.0",
|
||||
"url": "dummy-url" }'
|
||||
---
|
||||
```
|
||||
|
||||
#### Using a block scalar
|
||||
|
||||
The literal block scalar (`|`) preserves line breaks and is highly recommended
|
||||
for multiline JSON strings as it avoids quote escaping entirely. The following
|
||||
is a complete, valid Agent Card configuration using dummy values.
|
||||
|
||||
```markdown
|
||||
---
|
||||
kind: remote
|
||||
name: block-scalar-agent
|
||||
agent_card_json: |
|
||||
{
|
||||
"protocolVersion": "0.3.0",
|
||||
"name": "Example Agent Name",
|
||||
"description": "An example agent description for documentation purposes.",
|
||||
"version": "1.0.0",
|
||||
"url": "dummy-url",
|
||||
"preferredTransport": "HTTP+JSON",
|
||||
"capabilities": {
|
||||
"streaming": true,
|
||||
"extendedAgentCard": false
|
||||
},
|
||||
"defaultInputModes": [
|
||||
"text/plain"
|
||||
],
|
||||
"defaultOutputModes": [
|
||||
"application/json"
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"id": "ExampleSkill",
|
||||
"name": "Example Skill Assistant",
|
||||
"description": "A description of what this example skill does.",
|
||||
"tags": [
|
||||
"example-tag"
|
||||
],
|
||||
"examples": [
|
||||
"Show me an example."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
---
|
||||
```
|
||||
|
||||
#### Using double quotes
|
||||
|
||||
Double quotes are also supported, but any internal double quotes in your JSON
|
||||
must be escaped with a backslash.
|
||||
|
||||
```markdown
|
||||
---
|
||||
kind: remote
|
||||
name: double-quotes-agent
|
||||
agent_card_json:
|
||||
'{ "protocolVersion": "0.3.0", "name": "Example Agent", "version": "1.0.0",
|
||||
"url": "dummy-url" }'
|
||||
---
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Authentication
|
||||
|
||||
Many remote agents require authentication. Gemini CLI supports several
|
||||
@@ -194,7 +104,7 @@ Gemini CLI supports the following authentication types:
|
||||
| `apiKey` | Send a static API key as an HTTP header. |
|
||||
| `http` | HTTP authentication (Bearer token, Basic credentials, or any IANA-registered scheme). |
|
||||
| `google-credentials` | Google Application Default Credentials (ADC). Automatically selects access or identity tokens. |
|
||||
| `oauth` | OAuth 2.0 Authorization Code flow with PKCE. Opens a browser for interactive sign-in. |
|
||||
| `oauth2` | OAuth 2.0 Authorization Code flow with PKCE. Opens a browser for interactive sign-in. |
|
||||
|
||||
### Dynamic values
|
||||
|
||||
@@ -353,7 +263,7 @@ hosts:
|
||||
|
||||
Requests to any other host will be rejected with an error. If your agent is
|
||||
hosted on a different domain, use one of the other auth types (`apiKey`, `http`,
|
||||
or `oauth`).
|
||||
or `oauth2`).
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -387,7 +297,7 @@ auth:
|
||||
---
|
||||
```
|
||||
|
||||
### OAuth 2.0 (`oauth`)
|
||||
### OAuth 2.0 (`oauth2`)
|
||||
|
||||
Performs an interactive OAuth 2.0 Authorization Code flow with PKCE. On first
|
||||
use, Gemini CLI opens your browser for sign-in and persists the resulting tokens
|
||||
@@ -395,7 +305,7 @@ for subsequent requests.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :------------------ | :------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | string | Yes | Must be `oauth`. |
|
||||
| `type` | string | Yes | Must be `oauth2`. |
|
||||
| `client_id` | string | Yes\* | OAuth client ID. Required for interactive auth. |
|
||||
| `client_secret` | string | No\* | OAuth client secret. Required by most authorization servers (confidential clients). Can be omitted for public clients that don't require a secret. |
|
||||
| `scopes` | string[] | No | Requested scopes. Can also be discovered from the agent card. |
|
||||
@@ -408,7 +318,7 @@ kind: remote
|
||||
name: oauth-agent
|
||||
agent_card_url: https://example.com/.well-known/agent.json
|
||||
auth:
|
||||
type: oauth
|
||||
type: oauth2
|
||||
client_id: my-client-id.apps.example.com
|
||||
---
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ Once Gemini CLI is installed, run Gemini CLI from your command line:
|
||||
gemini
|
||||
```
|
||||
|
||||
For more installation options, see [Gemini CLI Installation](./installation/).
|
||||
For more installation options, see [Gemini CLI Installation](./installation.md).
|
||||
|
||||
## Authenticate
|
||||
|
||||
|
||||
@@ -23,33 +23,34 @@ installation methods, and release types.
|
||||
We recommend most users install Gemini CLI using one of the following
|
||||
installation methods:
|
||||
|
||||
{% tabs %}
|
||||
{% tabitem label="npm" %}
|
||||
Install globally with npm:
|
||||
- npm
|
||||
- Homebrew
|
||||
- MacPorts
|
||||
- Anaconda
|
||||
|
||||
Note that Gemini CLI comes pre-installed on
|
||||
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
|
||||
[**Cloud Workstations**](https://cloud.google.com/workstations).
|
||||
|
||||
### Install globally with npm
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
{% /tabitem %}
|
||||
|
||||
{% tabitem label="Homebrew" %}
|
||||
Install globally with Homebrew (macOS/Linux):
|
||||
### Install globally with Homebrew (macOS/Linux)
|
||||
|
||||
```bash
|
||||
brew install gemini-cli
|
||||
```
|
||||
{% /tabitem %}
|
||||
|
||||
{% tabitem label="MacPorts" %}
|
||||
Install globally with MacPorts (macOS):
|
||||
### Install globally with MacPorts (macOS)
|
||||
|
||||
```bash
|
||||
sudo port install gemini-cli
|
||||
```
|
||||
{% /tabitem %}
|
||||
|
||||
{% tabitem label="Anaconda" %}
|
||||
Install with Anaconda (for restricted environments):
|
||||
### Install with Anaconda (for restricted environments)
|
||||
|
||||
```bash
|
||||
# Create and activate a new environment
|
||||
@@ -59,12 +60,6 @@ conda activate gemini_env
|
||||
# Install Gemini CLI globally via npm (inside the environment)
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
{% /tabitem %}
|
||||
{% /tabs %}
|
||||
|
||||
Note that Gemini CLI comes pre-installed on
|
||||
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
|
||||
[**Cloud Workstations**](https://cloud.google.com/workstations).
|
||||
|
||||
## Run Gemini CLI
|
||||
|
||||
@@ -107,7 +102,7 @@ the default way that the CLI executes tools that might have side effects.
|
||||
to run the CLI.
|
||||
```bash
|
||||
# Run the published sandbox image
|
||||
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:latest
|
||||
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
|
||||
```
|
||||
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
|
||||
(using the standard installation described above), you can instruct it to run
|
||||
@@ -1,157 +0,0 @@
|
||||
# Gemini CLI installation, execution, and releases
|
||||
|
||||
This document provides an overview of Gemini CLI's system requirements,
|
||||
installation methods, and release types.
|
||||
|
||||
## Recommended system specifications
|
||||
|
||||
- **Operating System:**
|
||||
- macOS 15+
|
||||
- Windows 11 24H2+
|
||||
- Ubuntu 20.04+
|
||||
- **Hardware:**
|
||||
- "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits)
|
||||
- "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context)
|
||||
- **Runtime:** Node.js 20.0.0+
|
||||
- **Shell:** Bash, Zsh, or PowerShell
|
||||
- **Location:**
|
||||
[Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
|
||||
- **Internet connection required**
|
||||
|
||||
## Install Gemini CLI
|
||||
|
||||
We recommend most users install Gemini CLI using one of the following
|
||||
installation methods:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="npm">
|
||||
Install globally with npm:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Homebrew">
|
||||
Install globally with Homebrew (macOS/Linux):
|
||||
|
||||
```bash
|
||||
brew install gemini-cli
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Note that Gemini CLI comes pre-installed on
|
||||
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
|
||||
[**Cloud Workstations**](https://cloud.google.com/workstations).
|
||||
|
||||
## Run Gemini CLI
|
||||
|
||||
For most users, we recommend running Gemini CLI with the `gemini` command:
|
||||
|
||||
```bash
|
||||
gemini
|
||||
```
|
||||
|
||||
For a list of options and additional commands, see the
|
||||
[CLI cheatsheet](../cli/cli-reference.md).
|
||||
|
||||
You can also run Gemini CLI using one of the following advanced methods:
|
||||
|
||||
- Run instantly with npx. You can run Gemini CLI without permanent installation.
|
||||
- In a sandbox. This method offers increased security and isolation.
|
||||
- From the source. This is recommended for contributors to the project.
|
||||
|
||||
### Run instantly with npx
|
||||
|
||||
```bash
|
||||
# Using npx (no installation required)
|
||||
npx @google/gemini-cli
|
||||
```
|
||||
|
||||
You can also execute the CLI directly from the main branch on GitHub, which is
|
||||
helpful for testing features still in development:
|
||||
|
||||
```bash
|
||||
npx https://github.com/google-gemini/gemini-cli
|
||||
```
|
||||
|
||||
### Run in a sandbox (Docker/Podman)
|
||||
|
||||
For security and isolation, Gemini CLI can be run inside a container. This is
|
||||
the default way that the CLI executes tools that might have side effects.
|
||||
|
||||
- **Directly from the registry:** You can run the published sandbox image
|
||||
directly. This is useful for environments where you only have Docker and want
|
||||
to run the CLI.
|
||||
```bash
|
||||
# Run the published sandbox image
|
||||
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:latest
|
||||
```
|
||||
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
|
||||
(using the standard installation described above), you can instruct it to run
|
||||
inside the sandbox container.
|
||||
```bash
|
||||
gemini --sandbox -y -p "your prompt here"
|
||||
```
|
||||
|
||||
### Run from source (recommended for Gemini CLI contributors)
|
||||
|
||||
Contributors to the project will want to run the CLI directly from the source
|
||||
code.
|
||||
|
||||
- **Development mode:** This method provides hot-reloading and is useful for
|
||||
active development.
|
||||
```bash
|
||||
# From the root of the repository
|
||||
npm run start
|
||||
```
|
||||
- **Production-like mode (linked package):** This method simulates a global
|
||||
installation by linking your local package. It's useful for testing a local
|
||||
build in a production workflow.
|
||||
|
||||
```bash
|
||||
# Link the local cli package to your global node_modules
|
||||
npm link packages/cli
|
||||
|
||||
# Now you can run your local version using the `gemini` command
|
||||
gemini
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
Gemini CLI has three release channels: nightly, preview, and stable. For most
|
||||
users, we recommend the stable release, which is the default installation.
|
||||
|
||||
### Stable
|
||||
|
||||
New stable releases are published each week. The stable release is the promotion
|
||||
of last week's `preview` release along with any bug fixes. The stable release
|
||||
uses `latest` tag, but omitting the tag also installs the latest stable release
|
||||
by default:
|
||||
|
||||
```bash
|
||||
# Both commands install the latest stable release.
|
||||
npm install -g @google/gemini-cli
|
||||
npm install -g @google/gemini-cli@latest
|
||||
```
|
||||
|
||||
### Preview
|
||||
|
||||
New preview releases will be published each week. These releases are not fully
|
||||
vetted and may contain regressions or other outstanding issues. Try out the
|
||||
preview release by using the `preview` tag:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@preview
|
||||
```
|
||||
|
||||
### Nightly
|
||||
|
||||
Nightly releases are published every day. The nightly release includes all
|
||||
changes from the main branch at time of release. It should be assumed there are
|
||||
pending validations and issues. You can help test the latest changes by
|
||||
installing with the `nightly` tag:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli@nightly
|
||||
```
|
||||
+2
-2
@@ -15,8 +15,8 @@ npm install -g @google/gemini-cli
|
||||
Jump in to Gemini CLI.
|
||||
|
||||
- **[Quickstart](./get-started/index.md):** Your first session with Gemini CLI.
|
||||
- **[Installation](./get-started/installation/):** How to install Gemini CLI on
|
||||
your system.
|
||||
- **[Installation](./get-started/installation.md):** How to install Gemini CLI
|
||||
on your system.
|
||||
- **[Authentication](./get-started/authentication.md):** Setup instructions for
|
||||
personal and enterprise accounts.
|
||||
- **[Examples](./get-started/examples.md):** Practical examples of Gemini CLI in
|
||||
|
||||
@@ -250,8 +250,8 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **`list`** or **`ls`**:
|
||||
- **Description:** List configured MCP servers and tools. This is the
|
||||
default action if no subcommand is specified.
|
||||
- **`reload`**:
|
||||
- **Description:** Reloads all MCP servers and re-discovers their available
|
||||
- **`refresh`**:
|
||||
- **Description:** Restarts all MCP servers and re-discovers their available
|
||||
tools.
|
||||
- **`schema`**:
|
||||
- **Description:** List configured MCP servers and tools with descriptions
|
||||
|
||||
@@ -143,8 +143,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`general.plan.directory`** (string):
|
||||
- **Description:** The directory where planning artifacts are stored. If not
|
||||
specified, defaults to the system temporary directory. A custom directory
|
||||
requires a policy to allow write access in Plan Mode.
|
||||
specified, defaults to the system temporary directory.
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -296,11 +295,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Hide the footer from the UI
|
||||
- **Default:** `false`
|
||||
|
||||
- **`ui.collapseDrawerDuringApproval`** (boolean):
|
||||
- **Description:** Whether to collapse the UI drawer when a tool is awaiting
|
||||
confirmation.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.showMemoryUsage`** (boolean):
|
||||
- **Description:** Display memory usage information in the UI
|
||||
- **Default:** `false`
|
||||
@@ -646,11 +640,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"model": "gemini-3-flash-preview"
|
||||
}
|
||||
},
|
||||
"chat-compression-3.1-flash-lite": {
|
||||
"modelConfig": {
|
||||
"model": "gemini-3.1-flash-lite-preview"
|
||||
}
|
||||
},
|
||||
"chat-compression-2.5-pro": {
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-pro"
|
||||
@@ -855,12 +844,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"hasAccessToPreview": false
|
||||
},
|
||||
"target": "gemini-2.5-pro"
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"useCustomTools": true
|
||||
},
|
||||
"target": "gemini-3.1-pro-preview-customtools"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -985,17 +968,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
"auto-gemini-2.5": {
|
||||
"default": "gemini-2.5-pro"
|
||||
},
|
||||
"gemini-3.1-flash-lite-preview": {
|
||||
"default": "gemini-3.1-flash-lite-preview",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"useGemini3_1FlashLite": false
|
||||
},
|
||||
"target": "gemini-2.5-flash-lite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"flash": {
|
||||
"default": "gemini-3-flash-preview",
|
||||
"contexts": [
|
||||
@@ -1008,15 +980,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
]
|
||||
},
|
||||
"flash-lite": {
|
||||
"default": "gemini-2.5-flash-lite",
|
||||
"contexts": [
|
||||
{
|
||||
"condition": {
|
||||
"useGemini3_1FlashLite": true
|
||||
},
|
||||
"target": "gemini-3.1-flash-lite-preview"
|
||||
}
|
||||
]
|
||||
"default": "gemini-2.5-flash-lite"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1246,11 +1210,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Disable user input on browser window during automation.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`agents.browser.maxActionsPerTask`** (number):
|
||||
- **Description:** The maximum number of tool calls allowed per browser task.
|
||||
Enforcement is hard: the agent will be terminated when the limit is reached.
|
||||
- **Default:** `100`
|
||||
|
||||
- **`agents.browser.confirmSensitiveActions`** (boolean):
|
||||
- **Description:** Require manual confirmation for sensitive browser actions
|
||||
(e.g., fill_form, evaluate_script).
|
||||
@@ -2377,13 +2336,9 @@ can be based on the base sandbox image:
|
||||
```dockerfile
|
||||
FROM gemini-cli-sandbox
|
||||
|
||||
# Add your custom dependencies or configurations here.
|
||||
# Note: The base image runs as the non-root 'node' user.
|
||||
# You must switch to 'root' to install system packages.
|
||||
# Add your custom dependencies or configurations here
|
||||
# For example:
|
||||
# USER root
|
||||
# RUN apt-get update && apt-get install -y some-package
|
||||
# USER node
|
||||
# COPY ./my-config /app/my-config
|
||||
```
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ priority = 10
|
||||
# (Optional) A custom message to display when a tool call is denied by this
|
||||
# rule. This message is returned to the model and user,
|
||||
# useful for explaining *why* it was denied.
|
||||
denyMessage = "Deletion is permanent"
|
||||
deny_message = "Deletion is permanent"
|
||||
|
||||
# (Optional) An array of approval modes where this rule is active.
|
||||
modes = ["autoEdit"]
|
||||
@@ -310,14 +310,6 @@ modes = ["autoEdit"]
|
||||
# non-interactive (false) environments.
|
||||
# If omitted, the rule applies to both.
|
||||
interactive = true
|
||||
|
||||
# (Optional) If true, lets shell commands use redirection operators
|
||||
# (>, >>, <, <<, <<<). By default, the policy engine asks for confirmation
|
||||
# when redirection is detected, even if a rule matches the command.
|
||||
# This permission is granular; it only applies to the specific rule it's
|
||||
# defined in. In chained commands (e.g., cmd1 > file && cmd2), each
|
||||
# individual command rule must permit redirection if it's used.
|
||||
allowRedirection = true
|
||||
```
|
||||
|
||||
### Using arrays (lists)
|
||||
@@ -402,7 +394,7 @@ server.
|
||||
mcpName = "untrusted-server"
|
||||
decision = "deny"
|
||||
priority = 500
|
||||
denyMessage = "This server is not trusted by the admin."
|
||||
deny_message = "This server is not trusted by the admin."
|
||||
```
|
||||
|
||||
**3. Targeting all MCP servers**
|
||||
@@ -413,7 +405,6 @@ registered MCP server. This is useful for setting category-wide defaults.
|
||||
```toml
|
||||
# Ask user for any tool call from any MCP server
|
||||
[[rule]]
|
||||
toolName = "*"
|
||||
mcpName = "*"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
|
||||
+22
-55
@@ -63,62 +63,29 @@ details.
|
||||
|
||||
## Available tools
|
||||
|
||||
The following sections list all available tools, categorized by their primary
|
||||
function. For detailed parameter information, see the linked documentation for
|
||||
each tool.
|
||||
The following table lists all available tools, categorized by their primary
|
||||
function.
|
||||
|
||||
### Execution
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :--------------------------------------- | :-------- | :----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`run_shell_command`](../tools/shell.md) | `Execute` | Executes arbitrary shell commands. Supports interactive sessions and background processes. Requires manual confirmation. |
|
||||
|
||||
### File System
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------- |
|
||||
| [`glob`](../tools/file-system.md) | `Search` | Finds files matching specific glob patterns across the workspace. |
|
||||
| [`grep_search`](../tools/file-system.md) | `Search` | Searches for a regular expression pattern within file contents. Legacy alias: `search_file_content`. |
|
||||
| [`list_directory`](../tools/file-system.md) | `Read` | Lists the names of files and subdirectories within a specified path. |
|
||||
| [`read_file`](../tools/file-system.md) | `Read` | Reads the content of a specific file. Supports text, images, audio, and PDF. |
|
||||
| [`read_many_files`](../tools/file-system.md) | `Read` | Reads and concatenates content from multiple files. Often triggered by the `@` symbol in your prompt. |
|
||||
| [`replace`](../tools/file-system.md) | `Edit` | Performs precise text replacement within a file. Requires manual confirmation. |
|
||||
| [`write_file`](../tools/file-system.md) | `Edit` | Creates or overwrites a file with new content. Requires manual confirmation. |
|
||||
|
||||
### Interaction
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :--------------------------------- | :------------ | :------------------------------------------------------------------------------------- |
|
||||
| [`ask_user`](../tools/ask-user.md) | `Communicate` | Requests clarification or missing information via an interactive dialog. |
|
||||
| [`write_todos`](../tools/todos.md) | `Other` | Maintains an internal list of subtasks. The model uses this to track its own progress. |
|
||||
|
||||
### Memory
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :----------------------------------------------- | :------ | :----------------------------------------------------------------------------------- |
|
||||
| [`activate_skill`](../tools/activate-skill.md) | `Other` | Loads specialized procedural expertise from the `.gemini/skills` directory. |
|
||||
| [`get_internal_docs`](../tools/internal-docs.md) | `Think` | Accesses Gemini CLI's own documentation for accurate answers about its capabilities. |
|
||||
| [`save_memory`](../tools/memory.md) | `Think` | Persists specific facts and project details to your `GEMINI.md` file. |
|
||||
|
||||
### Planning
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :---------------------------------------- | :----- | :--------------------------------------------------------------------------------------- |
|
||||
| [`enter_plan_mode`](../tools/planning.md) | `Plan` | Switches the CLI to a safe, read-only "Plan Mode" for researching complex changes. |
|
||||
| [`exit_plan_mode`](../tools/planning.md) | `Plan` | Finalizes a plan, presents it for review, and requests approval to start implementation. |
|
||||
|
||||
### System
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :-------------- | :------ | :----------------------------------------------------------------------------------------------------------------- |
|
||||
| `complete_task` | `Other` | Finalizes a subagent's mission and returns the result to the parent agent. This tool is not available to the user. |
|
||||
|
||||
### Web
|
||||
|
||||
| Tool | Kind | Description |
|
||||
| :-------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`google_web_search`](../tools/web-search.md) | `Search` | Performs a Google Search to find up-to-date information. |
|
||||
| [`web_fetch`](../tools/web-fetch.md) | `Fetch` | Retrieves and processes content from specific URLs. **Warning:** This tool can access local and private network addresses (e.g., localhost), which may pose a security risk if used with untrusted prompts. |
|
||||
| Category | Tool | Kind | Description |
|
||||
| :---------- | :----------------------------------------------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Execution | [`run_shell_command`](../tools/shell.md) | `Execute` | Executes arbitrary shell commands. Supports interactive sessions and background processes. Requires manual confirmation.<br><br>**Parameters:** `command`, `description`, `dir_path`, `is_background` |
|
||||
| File System | [`glob`](../tools/file-system.md) | `Search` | Finds files matching specific glob patterns across the workspace.<br><br>**Parameters:** `pattern`, `dir_path`, `case_sensitive`, `respect_git_ignore`, `respect_gemini_ignore` |
|
||||
| File System | [`grep_search`](../tools/file-system.md) | `Search` | Searches for a regular expression pattern within file contents. Legacy alias: `search_file_content`.<br><br>**Parameters:** `pattern`, `dir_path`, `include`, `exclude_pattern`, `names_only`, `max_matches_per_file`, `total_max_matches` |
|
||||
| File System | [`list_directory`](../tools/file-system.md) | `Read` | Lists the names of files and subdirectories within a specified path.<br><br>**Parameters:** `dir_path`, `ignore`, `file_filtering_options` |
|
||||
| File System | [`read_file`](../tools/file-system.md) | `Read` | Reads the content of a specific file. Supports text, images, audio, and PDF.<br><br>**Parameters:** `file_path`, `start_line`, `end_line` |
|
||||
| File System | [`read_many_files`](../tools/file-system.md) | `Read` | Reads and concatenates content from multiple files. Often triggered by the `@` symbol in your prompt.<br><br>**Parameters:** `include`, `exclude`, `recursive`, `useDefaultExcludes`, `file_filtering_options` |
|
||||
| File System | [`replace`](../tools/file-system.md) | `Edit` | Performs precise text replacement within a file. Requires manual confirmation.<br><br>**Parameters:** `file_path`, `instruction`, `old_string`, `new_string`, `allow_multiple` |
|
||||
| File System | [`write_file`](../tools/file-system.md) | `Edit` | Creates or overwrites a file with new content. Requires manual confirmation.<br><br>**Parameters:** `file_path`, `content` |
|
||||
| Interaction | [`ask_user`](../tools/ask-user.md) | `Communicate` | Requests clarification or missing information via an interactive dialog.<br><br>**Parameters:** `questions` |
|
||||
| Interaction | [`write_todos`](../tools/todos.md) | `Other` | Maintains an internal list of subtasks. The model uses this to track its own progress and display it to you.<br><br>**Parameters:** `todos` |
|
||||
| Memory | [`activate_skill`](../tools/activate-skill.md) | `Other` | Loads specialized procedural expertise for specific tasks from the `.gemini/skills` directory.<br><br>**Parameters:** `name` |
|
||||
| Memory | [`get_internal_docs`](../tools/internal-docs.md) | `Think` | Accesses Gemini CLI's own documentation to provide more accurate answers about its capabilities.<br><br>**Parameters:** `path` |
|
||||
| Memory | [`save_memory`](../tools/memory.md) | `Think` | Persists specific facts and project details to your `GEMINI.md` file to retain context.<br><br>**Parameters:** `fact` |
|
||||
| Planning | [`enter_plan_mode`](../tools/planning.md) | `Plan` | Switches the CLI to a safe, read-only "Plan Mode" for researching complex changes.<br><br>**Parameters:** `reason` |
|
||||
| Planning | [`exit_plan_mode`](../tools/planning.md) | `Plan` | Finalizes a plan, presents it for review, and requests approval to start implementation.<br><br>**Parameters:** `plan` |
|
||||
| System | `complete_task` | `Other` | Finalizes a subagent's mission and returns the result to the parent agent. This tool is not available to the user.<br><br>**Parameters:** `result` |
|
||||
| Web | [`google_web_search`](../tools/web-search.md) | `Search` | Performs a Google Search to find up-to-date information.<br><br>**Parameters:** `query` |
|
||||
| Web | [`web_fetch`](../tools/web-fetch.md) | `Fetch` | Retrieves and processes content from specific URLs. **Warning:** This tool can access local and private network addresses (e.g., localhost), which may pose a security risk if used with untrusted prompts.<br><br>**Parameters:** `prompt` |
|
||||
|
||||
## Under the hood
|
||||
|
||||
|
||||
+54
-24
@@ -6,10 +6,6 @@ for changes to system prompts, tool definitions, and other model-steering
|
||||
mechanisms, and as a tool for assessing feature reliability by model, and
|
||||
preventing regressions.
|
||||
|
||||
> [!TIP] **Agent Automation**: If you are pair-programming with Gemini CLI, you
|
||||
> can leverage the **behavioral-evals skill** to automate fixing failing tests
|
||||
> or promoting incubation candidates.
|
||||
|
||||
## Why Behavioral Evals?
|
||||
|
||||
Unlike traditional **integration tests** which verify that the system functions
|
||||
@@ -125,7 +121,7 @@ import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('my_feature', () => {
|
||||
// New tests MUST start as USUALLY_PASSES and be promoted based on consistency metrics
|
||||
// New tests MUST start as USUALLY_PASSES and be promoted via /promote-behavioral-eval
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should do something',
|
||||
prompt: 'do it',
|
||||
@@ -187,10 +183,12 @@ mandatory deflaking process.
|
||||
|
||||
1. **Incubation**: You must create all new tests with the `USUALLY_PASSES`
|
||||
policy. This lets them be monitored in the nightly runs without blocking PRs.
|
||||
2. **Monitoring**: The test must complete at least 7 nightly runs across all
|
||||
2. **Monitoring**: The test must complete at least 10 nightly runs across all
|
||||
supported models.
|
||||
3. **Promotion**: Promotion to `ALWAYS_PASSES` is conducted by the agent after
|
||||
verifying the 100% success rate requirement is met across many runs.
|
||||
3. **Promotion**: Promotion to `ALWAYS_PASSES` happens exclusively through the
|
||||
`/promote-behavioral-eval` slash command. This command verifies the 100%
|
||||
success rate requirement is met across many runs before updating the test
|
||||
policy.
|
||||
|
||||
This promotion process is essential for preventing the introduction of flaky
|
||||
evaluations into the CI.
|
||||
@@ -227,21 +225,42 @@ tool definition has made the model's behavior less reliable.
|
||||
|
||||
## Fixing Evaluations
|
||||
|
||||
If an evaluation is failing or has a regressed pass rate, ask the agent to
|
||||
investigate and fix the issue using the **behavioral-evals skill**. The agent
|
||||
will automate the following process:
|
||||
If an evaluation is failing or has a regressed pass rate, you can use the
|
||||
`/fix-behavioral-eval` command within Gemini CLI to help investigate and fix the
|
||||
issue.
|
||||
|
||||
### `/fix-behavioral-eval`
|
||||
|
||||
This command is designed to automate the investigation and fixing process for
|
||||
failing evaluations. It will:
|
||||
|
||||
1. **Investigate**: Fetch the latest results from the nightly workflow using
|
||||
the `gh` CLI, identify the failing test, and review test trajectory logs in
|
||||
`evals/logs`.
|
||||
2. **Fix**: Suggest and apply targeted fixes to the prompt or tool definitions.
|
||||
It prioritizes minimal changes to `prompt.ts` and tool instructions,
|
||||
avoiding changing the test itself unless necessary.
|
||||
3. **Verify**: Re-run the test locally across multiple models to ensure
|
||||
stability.
|
||||
4. **Report**: Provide a summary of the success rate.
|
||||
It prioritizes minimal changes to `prompt.ts`, tool instructions, and
|
||||
modules that contribute to the prompt. It generally tries to avoid changing
|
||||
the test itself.
|
||||
3. **Verify**: Re-run the test 3 times across multiple models (e.g., Gemini
|
||||
3.0, Gemini 3 Flash, Gemini 2.5 Pro) to ensure stability and calculate a
|
||||
success rate.
|
||||
4. **Report**: Provide a summary of the success rate for each model and details
|
||||
on the applied fixes.
|
||||
|
||||
When investigating failures manually, you can enable verbose agent logs by
|
||||
To use it, run:
|
||||
|
||||
```bash
|
||||
gemini /fix-behavioral-eval
|
||||
```
|
||||
|
||||
You can also provide a link to a specific GitHub Action run or the name of a
|
||||
specific test to focus the investigation:
|
||||
|
||||
```bash
|
||||
gemini /fix-behavioral-eval https://github.com/google-gemini/gemini-cli/actions/runs/123456789
|
||||
```
|
||||
|
||||
When investigating failures manually, you can also enable verbose agent logs by
|
||||
setting the `GEMINI_DEBUG_LOG_FILE` environment variable.
|
||||
|
||||
### Best practices
|
||||
@@ -254,14 +273,25 @@ instrospecting on its prompt when asked the right questions.
|
||||
|
||||
## Promoting evaluations
|
||||
|
||||
Evaluations must be promoted from `USUALLY_PASSES` to `ALWAYS_PASSES` by the
|
||||
agent to ensure that the 100% success rate requirement is empirically met.
|
||||
Evaluations must be promoted from `USUALLY_PASSES` to `ALWAYS_PASSES`
|
||||
exclusively using the `/promote-behavioral-eval` slash command. Manual promotion
|
||||
is not allowed to ensure that the 100% success rate requirement is empirically
|
||||
met.
|
||||
|
||||
The agent automates the promotion by:
|
||||
### `/promote-behavioral-eval`
|
||||
|
||||
This command automates the promotion of stable tests by:
|
||||
|
||||
1. **Investigating**: Analyzing the results of the last 7 nightly runs on the
|
||||
`main` branch.
|
||||
2. **Criteria Check**: Ensuring tests passed 100% of the time for ALL enabled
|
||||
models.
|
||||
3. **Promotion**: Updating the test file's policy to `ALWAYS_PASSES`.
|
||||
`main` branch using the `gh` CLI.
|
||||
2. **Criteria Check**: Identifying tests that have passed 100% of the time for
|
||||
ALL enabled models across the entire 7-run history.
|
||||
3. **Promotion**: Updating the test file's policy from `USUALLY_PASSES` to
|
||||
`ALWAYS_PASSES`.
|
||||
4. **Verification**: Running the promoted test locally to ensure correctness.
|
||||
|
||||
To run it:
|
||||
|
||||
```bash
|
||||
gemini /promote-behavioral-eval
|
||||
```
|
||||
|
||||
@@ -79,7 +79,7 @@ export function appEvalTest(policy: EvalPolicy, evalCase: AppEvalCase) {
|
||||
}
|
||||
|
||||
// Render the app!
|
||||
await rig.render();
|
||||
rig.render();
|
||||
|
||||
// Wait for initial ready state
|
||||
await rig.waitForIdle();
|
||||
|
||||
@@ -136,32 +136,6 @@ describe('plan_mode', () => {
|
||||
expect(wasToolCalled, 'Expected exit_plan_mode tool to be called').toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const exitPlanCall = toolLogs.find(
|
||||
(log) => log.toolRequest.name === 'exit_plan_mode',
|
||||
);
|
||||
expect(
|
||||
exitPlanCall,
|
||||
'Expected to find exit_plan_mode in tool logs',
|
||||
).toBeDefined();
|
||||
|
||||
const args = JSON.parse(exitPlanCall!.toolRequest.args);
|
||||
expect(args.plan_filename, 'plan_filename should be a string').toBeTypeOf(
|
||||
'string',
|
||||
);
|
||||
expect(args.plan_filename, 'plan_filename should end with .md').toMatch(
|
||||
/\.md$/,
|
||||
);
|
||||
expect(
|
||||
args.plan_filename,
|
||||
'plan_filename should not be a path',
|
||||
).not.toContain('/');
|
||||
expect(
|
||||
args.plan_filename,
|
||||
'plan_filename should not be a path',
|
||||
).not.toContain('\\');
|
||||
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
@@ -225,30 +199,6 @@ describe('plan_mode', () => {
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
|
||||
const exitPlanCall = toolLogs.find(
|
||||
(log) => log.toolRequest.name === 'exit_plan_mode',
|
||||
);
|
||||
expect(
|
||||
exitPlanCall,
|
||||
'Expected to find exit_plan_mode in tool logs',
|
||||
).toBeDefined();
|
||||
|
||||
const args = JSON.parse(exitPlanCall!.toolRequest.args);
|
||||
expect(args.plan_filename, 'plan_filename should be a string').toBeTypeOf(
|
||||
'string',
|
||||
);
|
||||
expect(args.plan_filename, 'plan_filename should end with .md').toMatch(
|
||||
/\.md$/,
|
||||
);
|
||||
expect(
|
||||
args.plan_filename,
|
||||
'plan_filename should not be a path',
|
||||
).not.toContain('/');
|
||||
expect(
|
||||
args.plan_filename,
|
||||
'plan_filename should not be a path',
|
||||
).not.toContain('\\');
|
||||
|
||||
// Check if plan was written
|
||||
const planWrite = toolLogs.find(
|
||||
(log) =>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
describe('redundant_casts', () => {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should not add redundant or unsafe casts when modifying typescript code',
|
||||
files: {
|
||||
'src/cast_example.ts': `
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function processUser(user: User) {
|
||||
// Narrowed check
|
||||
console.log("Processing user: " + user.name);
|
||||
}
|
||||
|
||||
export function handleUnknown(data: unknown) {
|
||||
// Goal: log data.id if it exists
|
||||
console.log("Handling data");
|
||||
}
|
||||
|
||||
export function handleError() {
|
||||
try {
|
||||
throw new Error("fail");
|
||||
} catch (err) {
|
||||
// Goal: log err.message
|
||||
console.error("Error happened");
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
prompt: `
|
||||
1. In src/cast_example.ts, update processUser to return the name in uppercase.
|
||||
2. In handleUnknown, log the "id" property if "data" is an object that contains it.
|
||||
3. In handleError, log the error message from "err".
|
||||
`,
|
||||
assert: async (rig) => {
|
||||
const filePath = path.join(rig.testDir!, 'src/cast_example.ts');
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
// 1. Redundant Cast Check (Same type)
|
||||
// Bad: (user.name as string).toUpperCase()
|
||||
expect(content, 'Should not cast a known string to string').not.toContain(
|
||||
'as string',
|
||||
);
|
||||
|
||||
// 2. Unsafe Cast Check (Unknown object)
|
||||
// Bad: (data as any).id or (data as {id: string}).id
|
||||
expect(
|
||||
content,
|
||||
'Should not use unsafe casts for unknown property access',
|
||||
).not.toContain('as any');
|
||||
expect(
|
||||
content,
|
||||
'Should not use unsafe casts for unknown property access',
|
||||
).not.toContain('as {');
|
||||
|
||||
// 3. Unsafe Cast Check (Error handling)
|
||||
// Bad: (err as Error).message
|
||||
// Good: if (err instanceof Error) { ... }
|
||||
expect(
|
||||
content,
|
||||
'Should prefer instanceof over casting for errors',
|
||||
).not.toContain('as Error');
|
||||
|
||||
// Verify implementation
|
||||
expect(content).toContain('toUpperCase()');
|
||||
expect(content).toContain('message');
|
||||
expect(content).toContain('id');
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('Sandbox recovery', () => {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'attempts to use additional_permissions when operation not permitted',
|
||||
prompt:
|
||||
'Run ./script.sh. It will fail with "Operation not permitted". When it does, you must retry running it by passing the appropriate additional_permissions.',
|
||||
files: {
|
||||
'script.sh':
|
||||
'#!/bin/bash\necho "cat: /etc/shadow: Operation not permitted" >&2\nexit 1\n',
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const shellCalls = toolLogs.filter(
|
||||
(log) =>
|
||||
log.toolRequest?.name === 'run_shell_command' &&
|
||||
log.toolRequest?.args?.includes('script.sh'),
|
||||
);
|
||||
|
||||
// The agent should have tried running the command.
|
||||
expect(
|
||||
shellCalls.length,
|
||||
'Agent should have called run_shell_command',
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// Look for a call that includes additional_permissions.
|
||||
const hasAdditionalPermissions = shellCalls.some((call) => {
|
||||
const args =
|
||||
typeof call.toolRequest.args === 'string'
|
||||
? JSON.parse(call.toolRequest.args)
|
||||
: call.toolRequest.args;
|
||||
return args.additional_permissions !== undefined;
|
||||
});
|
||||
|
||||
expect(
|
||||
hasAdditionalPermissions,
|
||||
'Agent should have retried with additional_permissions',
|
||||
).toBe(true);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -227,136 +227,4 @@ describe('save_memory', () => {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const proactiveMemoryFromLongSession =
|
||||
'Agent saves preference from earlier in conversation history';
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: proactiveMemoryFromLongSession,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryManager: true },
|
||||
},
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
id: 'msg-1',
|
||||
type: 'user',
|
||||
content: [
|
||||
{
|
||||
text: 'By the way, I always prefer Vitest over Jest for testing in all my projects.',
|
||||
},
|
||||
],
|
||||
timestamp: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'msg-2',
|
||||
type: 'gemini',
|
||||
content: [{ text: 'Noted! What are you working on today?' }],
|
||||
timestamp: '2026-01-01T00:00:05Z',
|
||||
},
|
||||
{
|
||||
id: 'msg-3',
|
||||
type: 'user',
|
||||
content: [
|
||||
{
|
||||
text: "I'm debugging a failing API endpoint. The /users route returns a 500 error.",
|
||||
},
|
||||
],
|
||||
timestamp: '2026-01-01T00:01:00Z',
|
||||
},
|
||||
{
|
||||
id: 'msg-4',
|
||||
type: 'gemini',
|
||||
content: [
|
||||
{
|
||||
text: 'It looks like the database connection might not be initialized before the query runs.',
|
||||
},
|
||||
],
|
||||
timestamp: '2026-01-01T00:01:10Z',
|
||||
},
|
||||
{
|
||||
id: 'msg-5',
|
||||
type: 'user',
|
||||
content: [
|
||||
{ text: 'Good catch — I fixed the import and the route works now.' },
|
||||
],
|
||||
timestamp: '2026-01-01T00:02:00Z',
|
||||
},
|
||||
{
|
||||
id: 'msg-6',
|
||||
type: 'gemini',
|
||||
content: [{ text: 'Great! Anything else you would like to work on?' }],
|
||||
timestamp: '2026-01-01T00:02:05Z',
|
||||
},
|
||||
],
|
||||
prompt:
|
||||
'Please save any persistent preferences or facts about me from our conversation to memory.',
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall(
|
||||
'save_memory',
|
||||
undefined,
|
||||
(args) => /vitest/i.test(args),
|
||||
);
|
||||
expect(
|
||||
wasToolCalled,
|
||||
'Expected save_memory to be called with the Vitest preference from the conversation history',
|
||||
).toBe(true);
|
||||
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
|
||||
const memoryManagerRoutingPreferences =
|
||||
'Agent routes global and project preferences to memory';
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: memoryManagerRoutingPreferences,
|
||||
params: {
|
||||
settings: {
|
||||
experimental: { memoryManager: true },
|
||||
},
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
id: 'msg-1',
|
||||
type: 'user',
|
||||
content: [
|
||||
{
|
||||
text: 'I always use dark mode in all my editors and terminals.',
|
||||
},
|
||||
],
|
||||
timestamp: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'msg-2',
|
||||
type: 'gemini',
|
||||
content: [{ text: 'Got it, I will keep that in mind!' }],
|
||||
timestamp: '2026-01-01T00:00:05Z',
|
||||
},
|
||||
{
|
||||
id: 'msg-3',
|
||||
type: 'user',
|
||||
content: [
|
||||
{
|
||||
text: 'For this project specifically, we use 2-space indentation.',
|
||||
},
|
||||
],
|
||||
timestamp: '2026-01-01T00:01:00Z',
|
||||
},
|
||||
{
|
||||
id: 'msg-4',
|
||||
type: 'gemini',
|
||||
content: [
|
||||
{ text: 'Understood, 2-space indentation for this project.' },
|
||||
],
|
||||
timestamp: '2026-01-01T00:01:05Z',
|
||||
},
|
||||
],
|
||||
prompt: 'Please save the preferences I mentioned earlier to memory.',
|
||||
assert: async (rig, result) => {
|
||||
const wasToolCalled = await rig.waitForToolCall('save_memory');
|
||||
expect(wasToolCalled, 'Expected save_memory to be called').toBe(true);
|
||||
|
||||
assertModelHasOutput(result);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
+15
-155
@@ -4,21 +4,21 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { describe } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
const AGENT_DEFINITION = `---
|
||||
name: docs-agent
|
||||
description: An agent with expertise in updating documentation.
|
||||
tools:
|
||||
- read_file
|
||||
- write_file
|
||||
---
|
||||
|
||||
import { evalTest, TEST_AGENTS } from './test-helper.js';
|
||||
You are the docs agent. Update the documentation.
|
||||
`;
|
||||
|
||||
const INDEX_TS = 'export const add = (a: number, b: number) => a + b;\n';
|
||||
|
||||
function readProjectFile(
|
||||
rig: { testDir?: string },
|
||||
relativePath: string,
|
||||
): string {
|
||||
return fs.readFileSync(path.join(rig.testDir!, relativePath), 'utf8');
|
||||
}
|
||||
const INDEX_TS = 'export const add = (a: number, b: number) => a + b;';
|
||||
|
||||
describe('subagent eval test cases', () => {
|
||||
/**
|
||||
@@ -42,152 +42,12 @@ describe('subagent eval test cases', () => {
|
||||
},
|
||||
prompt: 'Please update README.md with a description of this library.',
|
||||
files: {
|
||||
...TEST_AGENTS.DOCS_AGENT.asFile(),
|
||||
'.gemini/agents/test-agent.md': AGENT_DEFINITION,
|
||||
'index.ts': INDEX_TS,
|
||||
'README.md': 'TODO: update the README.\n',
|
||||
'README.md': 'TODO: update the README.',
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
await rig.expectToolCallSuccess([TEST_AGENTS.DOCS_AGENT.name]);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that the outer agent does not over-delegate trivial work when
|
||||
* subagents are available. This helps catch orchestration overuse.
|
||||
*/
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should avoid delegating trivial direct edit work',
|
||||
params: {
|
||||
settings: {
|
||||
experimental: {
|
||||
enableAgents: true,
|
||||
agents: {
|
||||
overrides: {
|
||||
generalist: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt:
|
||||
'Rename the exported function in index.ts from add to sum and update the file directly.',
|
||||
files: {
|
||||
...TEST_AGENTS.DOCS_AGENT.asFile(),
|
||||
'index.ts': INDEX_TS,
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
const updatedIndex = readProjectFile(rig, 'index.ts');
|
||||
const toolLogs = rig.readToolLogs() as Array<{
|
||||
toolRequest: { name: string };
|
||||
}>;
|
||||
|
||||
expect(updatedIndex).toContain('export const sum =');
|
||||
expect(
|
||||
toolLogs.some(
|
||||
(l) => l.toolRequest.name === TEST_AGENTS.DOCS_AGENT.name,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
|
||||
false,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks that the outer agent prefers a more relevant specialist over a
|
||||
* broad generalist when both are available.
|
||||
*
|
||||
* This is meant to codify the "overusing Generalist" failure mode.
|
||||
*/
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should prefer relevant specialist over generalist',
|
||||
params: {
|
||||
settings: {
|
||||
experimental: {
|
||||
enableAgents: true,
|
||||
agents: {
|
||||
overrides: {
|
||||
generalist: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt: 'Please add a small test file that verifies add(1, 2) returns 3.',
|
||||
files: {
|
||||
...TEST_AGENTS.TESTING_AGENT.asFile(),
|
||||
'index.ts': INDEX_TS,
|
||||
'package.json': JSON.stringify(
|
||||
{
|
||||
name: 'subagent-eval-project',
|
||||
version: '1.0.0',
|
||||
type: 'module',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
const toolLogs = rig.readToolLogs() as Array<{
|
||||
toolRequest: { name: string };
|
||||
}>;
|
||||
|
||||
await rig.expectToolCallSuccess([TEST_AGENTS.TESTING_AGENT.name]);
|
||||
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
|
||||
false,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks cardinality and decomposition for a multi-surface task. The task
|
||||
* naturally spans docs and tests, so multiple specialists should be used.
|
||||
*/
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should use multiple relevant specialists for multi-surface task',
|
||||
params: {
|
||||
settings: {
|
||||
experimental: {
|
||||
enableAgents: true,
|
||||
agents: {
|
||||
overrides: {
|
||||
generalist: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt:
|
||||
'Add a short README description for this library and also add a test file that verifies add(1, 2) returns 3.',
|
||||
files: {
|
||||
...TEST_AGENTS.DOCS_AGENT.asFile(),
|
||||
...TEST_AGENTS.TESTING_AGENT.asFile(),
|
||||
'index.ts': INDEX_TS,
|
||||
'README.md': 'TODO: update the README.\n',
|
||||
'package.json': JSON.stringify(
|
||||
{
|
||||
name: 'subagent-eval-project',
|
||||
version: '1.0.0',
|
||||
type: 'module',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
assert: async (rig, _result) => {
|
||||
const toolLogs = rig.readToolLogs() as Array<{
|
||||
toolRequest: { name: string };
|
||||
}>;
|
||||
const readme = readProjectFile(rig, 'README.md');
|
||||
|
||||
await rig.expectToolCallSuccess([
|
||||
TEST_AGENTS.DOCS_AGENT.name,
|
||||
TEST_AGENTS.TESTING_AGENT.name,
|
||||
]);
|
||||
expect(readme).not.toContain('TODO: update the README.');
|
||||
expect(toolLogs.some((l) => l.toolRequest.name === 'generalist')).toBe(
|
||||
false,
|
||||
);
|
||||
await rig.expectToolCallSuccess(['docs-agent']);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
+1
-57
@@ -13,9 +13,6 @@ import { TestRig } from '@google/gemini-cli-test-utils';
|
||||
import {
|
||||
createUnauthorizedToolError,
|
||||
parseAgentMarkdown,
|
||||
Storage,
|
||||
getProjectHash,
|
||||
SESSION_FILE_PREFIX,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export * from '@google/gemini-cli-test-utils';
|
||||
@@ -120,57 +117,8 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
execSync('git commit --allow-empty -m "Initial commit"', execOptions);
|
||||
}
|
||||
|
||||
// If messages are provided, write a session file so --resume can load it.
|
||||
let sessionId: string | undefined;
|
||||
if (evalCase.messages) {
|
||||
sessionId =
|
||||
evalCase.sessionId ||
|
||||
`test-session-${crypto.randomUUID().slice(0, 8)}`;
|
||||
|
||||
// Temporarily set GEMINI_CLI_HOME so Storage writes to the same
|
||||
// directory the CLI subprocess will use (rig.homeDir).
|
||||
const originalGeminiHome = process.env['GEMINI_CLI_HOME'];
|
||||
process.env['GEMINI_CLI_HOME'] = rig.homeDir!;
|
||||
try {
|
||||
const storage = new Storage(fs.realpathSync(rig.testDir!));
|
||||
await storage.initialize();
|
||||
const chatsDir = path.join(storage.getProjectTempDir(), 'chats');
|
||||
fs.mkdirSync(chatsDir, { recursive: true });
|
||||
|
||||
const conversation = {
|
||||
sessionId,
|
||||
projectHash: getProjectHash(fs.realpathSync(rig.testDir!)),
|
||||
startTime: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
messages: evalCase.messages,
|
||||
};
|
||||
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.slice(0, 16)
|
||||
.replace(/:/g, '-');
|
||||
const filename = `${SESSION_FILE_PREFIX}${timestamp}-${sessionId.slice(0, 8)}.json`;
|
||||
fs.writeFileSync(
|
||||
path.join(chatsDir, filename),
|
||||
JSON.stringify(conversation, null, 2),
|
||||
);
|
||||
} catch (e) {
|
||||
// Storage initialization may fail in some environments; log and continue.
|
||||
console.warn('Failed to write session history:', e);
|
||||
} finally {
|
||||
// Restore original GEMINI_CLI_HOME.
|
||||
if (originalGeminiHome === undefined) {
|
||||
delete process.env['GEMINI_CLI_HOME'];
|
||||
} else {
|
||||
process.env['GEMINI_CLI_HOME'] = originalGeminiHome;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await rig.run({
|
||||
args: sessionId
|
||||
? ['--resume', sessionId, evalCase.prompt]
|
||||
: evalCase.prompt,
|
||||
args: evalCase.prompt,
|
||||
approvalMode: evalCase.approvalMode ?? 'yolo',
|
||||
timeout: evalCase.timeout,
|
||||
env: {
|
||||
@@ -271,10 +219,6 @@ export interface EvalCase {
|
||||
prompt: string;
|
||||
timeout?: number;
|
||||
files?: Record<string, string>;
|
||||
/** Conversation history to pre-load via --resume. Each entry is a message object with type, content, etc. */
|
||||
messages?: Record<string, unknown>[];
|
||||
/** Session ID for the resumed session. Auto-generated if not provided. */
|
||||
sessionId?: string;
|
||||
approvalMode?: 'default' | 'auto_edit' | 'yolo' | 'plan';
|
||||
assert: (rig: TestRig, result: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as os from 'node:os';
|
||||
import { TestRig, skipFlaky } from './test-helper.js';
|
||||
import { TestRig } from './test-helper.js';
|
||||
|
||||
describe.skipIf(skipFlaky)('Ctrl+C exit', () => {
|
||||
describe('Ctrl+C exit', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
+2105
-2132
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, checkModelOutputContent } from './test-helper.js';
|
||||
import { TestRig, checkModelOutputContent, GEMINI_DIR } from './test-helper.js';
|
||||
|
||||
describe('Plan Mode', () => {
|
||||
let rig: TestRig;
|
||||
@@ -34,24 +36,28 @@ describe('Plan Mode', () => {
|
||||
},
|
||||
);
|
||||
|
||||
// We use a prompt that asks for both a read-only action and a write action.
|
||||
// "List files" (read-only) followed by "touch denied.txt" (write).
|
||||
const result = await rig.run({
|
||||
approvalMode: 'plan',
|
||||
args: 'Please list the files in the current directory, and then attempt to create a new file named "denied.txt" using a shell command.',
|
||||
stdin:
|
||||
'Please list the files in the current directory, and then attempt to create a new file named "denied.txt" using a shell command.',
|
||||
});
|
||||
|
||||
const lsCallFound = await rig.waitForToolCall('list_directory');
|
||||
expect(lsCallFound, 'Expected list_directory to be called').toBe(true);
|
||||
|
||||
const shellCallFound = await rig.waitForToolCall('run_shell_command');
|
||||
expect(shellCallFound, 'Expected run_shell_command to fail').toBe(false);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const lsLog = toolLogs.find((l) => l.toolRequest.name === 'list_directory');
|
||||
const shellLog = toolLogs.find(
|
||||
(l) => l.toolRequest.name === 'run_shell_command',
|
||||
);
|
||||
|
||||
expect(lsLog, 'Expected list_directory to be called').toBeDefined();
|
||||
expect(lsLog?.toolRequest.success).toBe(true);
|
||||
expect(
|
||||
shellLog,
|
||||
'Expected run_shell_command to be blocked (not even called)',
|
||||
toolLogs.find((l) => l.toolRequest.name === 'run_shell_command'),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(lsLog?.toolRequest.success).toBe(true);
|
||||
|
||||
checkModelOutputContent(result, {
|
||||
expectedContent: ['Plan Mode', 'read-only'],
|
||||
testName: 'Plan Mode restrictions test',
|
||||
@@ -78,11 +84,23 @@ describe('Plan Mode', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await rig.run({
|
||||
// Disable the interactive terminal setup prompt in tests
|
||||
writeFileSync(
|
||||
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
|
||||
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
|
||||
);
|
||||
|
||||
const run = await rig.runInteractive({
|
||||
approvalMode: 'plan',
|
||||
args: 'Create a file called plan.md in the plans directory.',
|
||||
});
|
||||
|
||||
await run.type('Create a file called plan.md in the plans directory.');
|
||||
await run.type('\r');
|
||||
|
||||
await rig.expectToolCallSuccess(['write_file'], 30000, (args) =>
|
||||
args.includes('plan.md'),
|
||||
);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const planWrite = toolLogs.find(
|
||||
(l) =>
|
||||
@@ -90,25 +108,7 @@ describe('Plan Mode', () => {
|
||||
l.toolRequest.args.includes('plans') &&
|
||||
l.toolRequest.args.includes('plan.md'),
|
||||
);
|
||||
|
||||
if (!planWrite) {
|
||||
console.error(
|
||||
'All tool calls found:',
|
||||
toolLogs.map((l) => ({
|
||||
name: l.toolRequest.name,
|
||||
args: l.toolRequest.args,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
expect(
|
||||
planWrite,
|
||||
'Expected write_file to be called for plan.md',
|
||||
).toBeDefined();
|
||||
expect(
|
||||
planWrite?.toolRequest.success,
|
||||
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
|
||||
).toBe(true);
|
||||
expect(planWrite?.toolRequest.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should deny write_file to non-plans directory in plan mode', async () => {
|
||||
@@ -131,11 +131,19 @@ describe('Plan Mode', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await rig.run({
|
||||
// Disable the interactive terminal setup prompt in tests
|
||||
writeFileSync(
|
||||
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
|
||||
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
|
||||
);
|
||||
|
||||
const run = await rig.runInteractive({
|
||||
approvalMode: 'plan',
|
||||
args: 'Create a file called hello.txt in the current directory.',
|
||||
});
|
||||
|
||||
await run.type('Create a file called hello.txt in the current directory.');
|
||||
await run.type('\r');
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const writeLog = toolLogs.find(
|
||||
(l) =>
|
||||
@@ -143,11 +151,10 @@ describe('Plan Mode', () => {
|
||||
l.toolRequest.args.includes('hello.txt'),
|
||||
);
|
||||
|
||||
// In Plan Mode, writes outside the plans directory should be blocked.
|
||||
// Model is undeterministic, sometimes it doesn't even try, but if it does, it must fail.
|
||||
if (writeLog) {
|
||||
expect(
|
||||
writeLog.toolRequest.success,
|
||||
'Expected write_file to non-plans dir to fail',
|
||||
).toBe(false);
|
||||
expect(writeLog.toolRequest.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -162,69 +169,28 @@ describe('Plan Mode', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Disable the interactive terminal setup prompt in tests
|
||||
writeFileSync(
|
||||
join(rig.homeDir!, GEMINI_DIR, 'state.json'),
|
||||
JSON.stringify({ terminalSetupPromptShown: true }, null, 2),
|
||||
);
|
||||
|
||||
// Start in default mode and ask to enter plan mode.
|
||||
await rig.run({
|
||||
approvalMode: 'default',
|
||||
args: 'I want to perform a complex refactoring. Please enter plan mode so we can design it first.',
|
||||
stdin:
|
||||
'I want to perform a complex refactoring. Please enter plan mode so we can design it first.',
|
||||
});
|
||||
|
||||
const enterPlanCallFound = await rig.waitForToolCall('enter_plan_mode');
|
||||
expect(enterPlanCallFound, 'Expected enter_plan_mode to be called').toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const enterLog = toolLogs.find(
|
||||
(l) => l.toolRequest.name === 'enter_plan_mode',
|
||||
);
|
||||
expect(enterLog, 'Expected enter_plan_mode to be called').toBeDefined();
|
||||
expect(enterLog?.toolRequest.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow write_file to the plans directory in plan mode even without a session ID', async () => {
|
||||
const plansDir = '.gemini/tmp/foo/plans';
|
||||
const testName =
|
||||
'should allow write_file to the plans directory in plan mode even without a session ID';
|
||||
|
||||
await rig.setup(testName, {
|
||||
settings: {
|
||||
experimental: { plan: true },
|
||||
tools: {
|
||||
core: ['write_file', 'read_file', 'list_directory'],
|
||||
},
|
||||
general: {
|
||||
defaultApprovalMode: 'plan',
|
||||
plan: {
|
||||
directory: plansDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await rig.run({
|
||||
approvalMode: 'plan',
|
||||
args: 'Create a file called plan-no-session.md in the plans directory.',
|
||||
});
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const planWrite = toolLogs.find(
|
||||
(l) =>
|
||||
l.toolRequest.name === 'write_file' &&
|
||||
l.toolRequest.args.includes('plans') &&
|
||||
l.toolRequest.args.includes('plan-no-session.md'),
|
||||
);
|
||||
|
||||
if (!planWrite) {
|
||||
console.error(
|
||||
'All tool calls found:',
|
||||
toolLogs.map((l) => ({
|
||||
name: l.toolRequest.name,
|
||||
args: l.toolRequest.args,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
expect(
|
||||
planWrite,
|
||||
'Expected write_file to be called for plan-no-session.md',
|
||||
).toBeDefined();
|
||||
expect(
|
||||
planWrite?.toolRequest.success,
|
||||
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,17 +183,11 @@ describe('Policy Engine Headless Mode', () => {
|
||||
responsesFile: 'policy-headless-shell-denied.responses',
|
||||
promptCommand: ECHO_PROMPT,
|
||||
policyContent: `
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "echo"
|
||||
decision = "deny"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "node"
|
||||
decision = "allow"
|
||||
priority = 90
|
||||
priority = 100
|
||||
`,
|
||||
expectAllowed: false,
|
||||
expectedDenialString: 'Tool execution denied by policy',
|
||||
|
||||
@@ -58,18 +58,12 @@ function getDisallowedFileReadCommand(testFile: string): {
|
||||
const quotedPath = `"${testFile}"`;
|
||||
switch (shell) {
|
||||
case 'powershell':
|
||||
return {
|
||||
command: `powershell -Command "Get-Content ${quotedPath}"`,
|
||||
tool: 'powershell',
|
||||
};
|
||||
return { command: `Get-Content ${quotedPath}`, tool: 'Get-Content' };
|
||||
case 'cmd':
|
||||
return { command: `cmd /c type ${quotedPath}`, tool: 'cmd' };
|
||||
return { command: `type ${quotedPath}`, tool: 'type' };
|
||||
case 'bash':
|
||||
default:
|
||||
return {
|
||||
command: `node -e "console.log(require('fs').readFileSync('${testFile}', 'utf8'))"`,
|
||||
tool: 'node',
|
||||
};
|
||||
return { command: `cat ${quotedPath}`, tool: 'cat' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, InteractiveRun, skipFlaky } from './test-helper.js';
|
||||
import { TestRig, InteractiveRun } from './test-helper.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import {
|
||||
@@ -33,107 +33,104 @@ const otherExtension = `{
|
||||
"version": "6.6.6"
|
||||
}`;
|
||||
|
||||
describe.skipIf(skipFlaky)(
|
||||
'extension symlink install spoofing protection',
|
||||
() => {
|
||||
let rig: TestRig;
|
||||
describe('extension symlink install spoofing protection', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('canonicalizes the trust path and prevents symlink spoofing', async () => {
|
||||
// Enable folder trust for this test
|
||||
rig.setup('symlink spoofing test', {
|
||||
settings: {
|
||||
security: {
|
||||
folderTrust: {
|
||||
enabled: true,
|
||||
},
|
||||
it('canonicalizes the trust path and prevents symlink spoofing', async () => {
|
||||
// Enable folder trust for this test
|
||||
rig.setup('symlink spoofing test', {
|
||||
settings: {
|
||||
security: {
|
||||
folderTrust: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const realExtPath = join(rig.testDir!, 'real-extension');
|
||||
mkdirSync(realExtPath);
|
||||
writeFileSync(join(realExtPath, 'gemini-extension.json'), extension);
|
||||
|
||||
const maliciousExtPath = join(
|
||||
os.tmpdir(),
|
||||
`malicious-extension-${Date.now()}`,
|
||||
);
|
||||
mkdirSync(maliciousExtPath);
|
||||
writeFileSync(
|
||||
join(maliciousExtPath, 'gemini-extension.json'),
|
||||
otherExtension,
|
||||
);
|
||||
|
||||
const symlinkPath = join(rig.testDir!, 'symlink-extension');
|
||||
symlinkSync(realExtPath, symlinkPath);
|
||||
|
||||
// Function to run a command with a PTY to avoid headless mode
|
||||
const runPty = (args: string[]) => {
|
||||
const ptyProcess = pty.spawn(process.execPath, [BUNDLE_PATH, ...args], {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 80,
|
||||
cwd: rig.testDir!,
|
||||
env: {
|
||||
...process.env,
|
||||
GEMINI_CLI_HOME: rig.homeDir!,
|
||||
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
||||
GEMINI_PTY_INFO: 'node-pty',
|
||||
},
|
||||
});
|
||||
return new InteractiveRun(ptyProcess);
|
||||
};
|
||||
|
||||
const realExtPath = join(rig.testDir!, 'real-extension');
|
||||
mkdirSync(realExtPath);
|
||||
writeFileSync(join(realExtPath, 'gemini-extension.json'), extension);
|
||||
// 1. Install via symlink, trust it
|
||||
const run1 = runPty(['extensions', 'install', symlinkPath]);
|
||||
await run1.expectText('Do you want to trust this folder', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('trust this workspace', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('Do you want to continue', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('installed successfully', 30000);
|
||||
await run1.kill();
|
||||
|
||||
const maliciousExtPath = join(
|
||||
os.tmpdir(),
|
||||
`malicious-extension-${Date.now()}`,
|
||||
);
|
||||
mkdirSync(maliciousExtPath);
|
||||
writeFileSync(
|
||||
join(maliciousExtPath, 'gemini-extension.json'),
|
||||
otherExtension,
|
||||
);
|
||||
// 2. Verify trustedFolders.json contains the REAL path, not the symlink path
|
||||
const trustedFoldersPath = join(
|
||||
rig.homeDir!,
|
||||
GEMINI_DIR,
|
||||
'trustedFolders.json',
|
||||
);
|
||||
// Wait for file to be written
|
||||
let attempts = 0;
|
||||
while (!fs.existsSync(trustedFoldersPath) && attempts < 50) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
const symlinkPath = join(rig.testDir!, 'symlink-extension');
|
||||
symlinkSync(realExtPath, symlinkPath);
|
||||
const trustedFolders = JSON.parse(
|
||||
readFileSync(trustedFoldersPath, 'utf-8'),
|
||||
);
|
||||
const trustedPaths = Object.keys(trustedFolders);
|
||||
const canonicalRealExtPath = fs.realpathSync(realExtPath);
|
||||
|
||||
// Function to run a command with a PTY to avoid headless mode
|
||||
const runPty = (args: string[]) => {
|
||||
const ptyProcess = pty.spawn(process.execPath, [BUNDLE_PATH, ...args], {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 80,
|
||||
cwd: rig.testDir!,
|
||||
env: {
|
||||
...process.env,
|
||||
GEMINI_CLI_HOME: rig.homeDir!,
|
||||
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
||||
GEMINI_PTY_INFO: 'node-pty',
|
||||
},
|
||||
});
|
||||
return new InteractiveRun(ptyProcess);
|
||||
};
|
||||
expect(trustedPaths).toContain(canonicalRealExtPath);
|
||||
expect(trustedPaths).not.toContain(symlinkPath);
|
||||
|
||||
// 1. Install via symlink, trust it
|
||||
const run1 = runPty(['extensions', 'install', symlinkPath]);
|
||||
await run1.expectText('Do you want to trust this folder', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('trust this workspace', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('Do you want to continue', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('installed successfully', 30000);
|
||||
await run1.kill();
|
||||
// 3. Swap the symlink to point to the malicious extension
|
||||
unlinkSync(symlinkPath);
|
||||
symlinkSync(maliciousExtPath, symlinkPath);
|
||||
|
||||
// 2. Verify trustedFolders.json contains the REAL path, not the symlink path
|
||||
const trustedFoldersPath = join(
|
||||
rig.homeDir!,
|
||||
GEMINI_DIR,
|
||||
'trustedFolders.json',
|
||||
);
|
||||
// Wait for file to be written
|
||||
let attempts = 0;
|
||||
while (!fs.existsSync(trustedFoldersPath) && attempts < 50) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
const trustedFolders = JSON.parse(
|
||||
readFileSync(trustedFoldersPath, 'utf-8'),
|
||||
);
|
||||
const trustedPaths = Object.keys(trustedFolders);
|
||||
const canonicalRealExtPath = fs.realpathSync(realExtPath);
|
||||
|
||||
expect(trustedPaths).toContain(canonicalRealExtPath);
|
||||
expect(trustedPaths).not.toContain(symlinkPath);
|
||||
|
||||
// 3. Swap the symlink to point to the malicious extension
|
||||
unlinkSync(symlinkPath);
|
||||
symlinkSync(maliciousExtPath, symlinkPath);
|
||||
|
||||
// 4. Try to install again via the same symlink path.
|
||||
// It should NOT be trusted because the real path changed.
|
||||
const run2 = runPty(['extensions', 'install', symlinkPath]);
|
||||
await run2.expectText('Do you want to trust this folder', 30000);
|
||||
await run2.type('n\r');
|
||||
await run2.expectText('Installation aborted', 30000);
|
||||
await run2.kill();
|
||||
}, 60000);
|
||||
},
|
||||
);
|
||||
// 4. Try to install again via the same symlink path.
|
||||
// It should NOT be trusted because the real path changed.
|
||||
const run2 = runPty(['extensions', 'install', symlinkPath]);
|
||||
await run2.expectText('Do you want to trust this folder', 30000);
|
||||
await run2.type('n\r');
|
||||
await run2.expectText('Installation aborted', 30000);
|
||||
await run2.kill();
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
@@ -6,5 +6,3 @@
|
||||
|
||||
export * from '@google/gemini-cli-test-utils';
|
||||
export { normalizePath } from '@google/gemini-cli-test-utils';
|
||||
|
||||
export const skipFlaky = !process.env['RUN_FLAKY_INTEGRATION'];
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"mcp_weather-server_get_weather","args":{"location":"London"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The weather in London is rainy."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
|
||||
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
TestRig,
|
||||
assertModelHasOutput,
|
||||
TestMcpServerBuilder,
|
||||
} from './test-helper.js';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe('test-mcp-support', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should discover and call a tool on the test server', async () => {
|
||||
await rig.setup('test-mcp-test', {
|
||||
settings: {
|
||||
tools: { core: [] }, // disable core tools to force using MCP
|
||||
model: {
|
||||
name: 'gemini-3-flash-preview',
|
||||
},
|
||||
},
|
||||
fakeResponsesPath: join(__dirname, 'test-mcp-support.responses'),
|
||||
});
|
||||
|
||||
// Workaround for ProjectRegistry save issue
|
||||
const userGeminiDir = join(rig.homeDir!, '.gemini');
|
||||
fs.writeFileSync(join(userGeminiDir, 'projects.json'), '{"projects":{}}');
|
||||
|
||||
const builder = new TestMcpServerBuilder('weather-server').addTool(
|
||||
'get_weather',
|
||||
'Get the weather for a location',
|
||||
'The weather in London is always rainy.',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: { type: 'string' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
rig.addTestMcpServer('weather-server', builder.build());
|
||||
|
||||
// Run the CLI asking for weather
|
||||
const output = await rig.run({
|
||||
args: 'What is the weather in London? Answer with the raw tool response snippet.',
|
||||
env: { GEMINI_API_KEY: 'dummy' },
|
||||
});
|
||||
|
||||
// Assert tool call
|
||||
const foundToolCall = await rig.waitForToolCall(
|
||||
'mcp_weather-server_get_weather',
|
||||
);
|
||||
expect(
|
||||
foundToolCall,
|
||||
'Expected to find a get_weather tool call',
|
||||
).toBeTruthy();
|
||||
|
||||
assertModelHasOutput(output);
|
||||
expect(output.toLowerCase()).toContain('rainy');
|
||||
}, 30000);
|
||||
});
|
||||
Generated
+21
-21
@@ -8696,9 +8696,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.2.tgz",
|
||||
"integrity": "sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -8711,9 +8711,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.5.9",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz",
|
||||
"integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.3.tgz",
|
||||
"integrity": "sha512-Ymnuefk6VzAhT3SxLzVUw+nMio/wB1NGypHkgetwtXcK1JfryaHk4DWQFGVwQ9XgzyS5iRZ7C2ZGI4AMsdMZ6A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -8722,9 +8722,9 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.2.0",
|
||||
"strnum": "^2.2.2"
|
||||
"fast-xml-builder": "^1.1.2",
|
||||
"path-expression-matcher": "^1.1.3",
|
||||
"strnum": "^2.1.2"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
@@ -8900,9 +8900,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -13200,9 +13200,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz",
|
||||
"integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz",
|
||||
"integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -15465,9 +15465,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
|
||||
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
|
||||
"integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -16469,9 +16469,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.24.5",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz",
|
||||
"integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.19.0.tgz",
|
||||
"integrity": "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"test:all_evals": "cross-env RUN_EVALS=1 vitest run --config evals/vitest.config.ts",
|
||||
"test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none",
|
||||
"test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman",
|
||||
"test:integration:flaky": "cross-env RUN_FLAKY_INTEGRATION=1 npm run test:integration:sandbox:none",
|
||||
"test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests",
|
||||
|
||||
@@ -29,7 +29,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
PRIORITY_YOLO_ALLOW_ALL: 998,
|
||||
Config: vi.fn().mockImplementation((params) => {
|
||||
const mockConfig = {
|
||||
...params,
|
||||
|
||||
@@ -87,7 +87,6 @@ export async function loadConfig(
|
||||
approvalMode === ApprovalMode.YOLO
|
||||
? [
|
||||
{
|
||||
toolName: '*',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: PRIORITY_YOLO_ALLOW_ALL,
|
||||
modes: [ApprovalMode.YOLO],
|
||||
|
||||
@@ -97,7 +97,6 @@ export function createMockConfig(
|
||||
getMcpClientManager: vi.fn().mockReturnValue({
|
||||
getMcpServers: vi.fn().mockReturnValue({}),
|
||||
}),
|
||||
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||
getGitService: vi.fn(),
|
||||
validatePathAccess: vi.fn().mockReturnValue(undefined),
|
||||
getShellExecutionConfig: vi.fn().mockReturnValue({
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
- **Shortcuts**: only define keyboard shortcuts in
|
||||
`packages/cli/src/ui/key/keyBindings.ts`
|
||||
- Do not implement any logic performing custom string measurement or string
|
||||
truncation. Use Ink layout instead leveraging ResizeObserver as needed. When
|
||||
using `ResizeObserver`, prefer the `useCallback` ref pattern (as seen in
|
||||
`MaxSizedBox.tsx`) to ensure size measurements are captured as soon as the
|
||||
element is available, avoiding potential rendering timing issues.
|
||||
truncation. Use Ink layout instead leveraging ResizeObserver as needed.
|
||||
- Avoid prop drilling when at all possible.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -21,13 +21,13 @@ import {
|
||||
AuthType,
|
||||
ToolConfirmationOutcome,
|
||||
StreamEventType,
|
||||
isWithinRoot,
|
||||
ReadManyFilesTool,
|
||||
type GeminiChat,
|
||||
type Config,
|
||||
type MessageBus,
|
||||
LlmRole,
|
||||
type GitService,
|
||||
processSingleFileContent,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
SettingScope,
|
||||
@@ -111,6 +111,7 @@ vi.mock(
|
||||
}),
|
||||
})),
|
||||
logToolCall: vi.fn(),
|
||||
isWithinRoot: vi.fn().mockReturnValue(true),
|
||||
LlmRole: {
|
||||
MAIN: 'main',
|
||||
SUBAGENT: 'subagent',
|
||||
@@ -133,7 +134,6 @@ vi.mock(
|
||||
Cancelled: 'cancelled',
|
||||
AwaitingApproval: 'awaiting_approval',
|
||||
},
|
||||
processSingleFileContent: vi.fn(),
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -177,10 +177,6 @@ describe('GeminiAgent', () => {
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
validatePathAccess: vi.fn().mockReturnValue(null),
|
||||
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||
addReadOnlyPath: vi.fn(),
|
||||
}),
|
||||
get config() {
|
||||
return this;
|
||||
},
|
||||
@@ -195,7 +191,6 @@ describe('GeminiAgent', () => {
|
||||
mockArgv = {} as unknown as CliArgs;
|
||||
mockConnection = {
|
||||
sessionUpdate: vi.fn(),
|
||||
requestPermission: vi.fn(),
|
||||
} as unknown as Mocked<acp.AgentSideConnection>;
|
||||
|
||||
(loadCliConfig as unknown as Mock).mockResolvedValue(mockConfig);
|
||||
@@ -653,7 +648,6 @@ describe('Session', () => {
|
||||
shouldIgnoreFile: vi.fn().mockReturnValue(false),
|
||||
}),
|
||||
getFileFilteringOptions: vi.fn().mockReturnValue({}),
|
||||
getFileSystemService: vi.fn().mockReturnValue({}),
|
||||
getTargetDir: vi.fn().mockReturnValue('/tmp'),
|
||||
getEnableRecursiveFileSearch: vi.fn().mockReturnValue(false),
|
||||
getDebugMode: vi.fn().mockReturnValue(false),
|
||||
@@ -663,10 +657,6 @@ describe('Session', () => {
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getGitService: vi.fn().mockResolvedValue({} as GitService),
|
||||
validatePathAccess: vi.fn().mockReturnValue(null),
|
||||
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||
addReadOnlyPath: vi.fn(),
|
||||
}),
|
||||
waitForMcpInit: vi.fn(),
|
||||
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
|
||||
get config() {
|
||||
@@ -1090,70 +1080,6 @@ describe('Session', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should split getDisplayTitle and getExplanation for title and content in permission request', async () => {
|
||||
const confirmationDetails = {
|
||||
type: 'info',
|
||||
onConfirm: vi.fn(),
|
||||
};
|
||||
mockTool.build.mockReturnValue({
|
||||
getDescription: () => 'Original Description',
|
||||
getDisplayTitle: () => 'Display Title Only',
|
||||
getExplanation: () => 'A detailed explanation text',
|
||||
toolLocations: () => [],
|
||||
shouldConfirmExecute: vi.fn().mockResolvedValue(confirmationDetails),
|
||||
execute: vi.fn().mockResolvedValue({ llmContent: 'Tool Result' }),
|
||||
});
|
||||
|
||||
mockConnection.requestPermission.mockResolvedValue({
|
||||
outcome: {
|
||||
outcome: 'selected',
|
||||
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||
},
|
||||
});
|
||||
|
||||
const stream1 = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: {
|
||||
functionCalls: [{ name: 'test_tool', args: {} }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const stream2 = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: { candidates: [] },
|
||||
},
|
||||
]);
|
||||
|
||||
mockChat.sendMessageStream
|
||||
.mockResolvedValueOnce(stream1)
|
||||
.mockResolvedValueOnce(stream2);
|
||||
|
||||
await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [{ type: 'text', text: 'Call tool' }],
|
||||
});
|
||||
|
||||
expect(mockConnection.requestPermission).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
toolCall: expect.objectContaining({
|
||||
title: 'Display Title Only',
|
||||
content: [],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
update: expect.objectContaining({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: { type: 'text', text: 'A detailed explanation text' },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use filePath for ACP diff content in tool result', async () => {
|
||||
mockTool.build.mockReturnValue({
|
||||
getDescription: () => 'Test Tool',
|
||||
@@ -1366,6 +1292,7 @@ describe('Session', () => {
|
||||
(fs.stat as unknown as Mock).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
});
|
||||
(isWithinRoot as unknown as Mock).mockReturnValue(true);
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
@@ -1423,6 +1350,7 @@ describe('Session', () => {
|
||||
(fs.stat as unknown as Mock).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
});
|
||||
(isWithinRoot as unknown as Mock).mockReturnValue(true);
|
||||
|
||||
const MockReadManyFilesTool = ReadManyFilesTool as unknown as Mock;
|
||||
MockReadManyFilesTool.mockImplementationOnce(() => ({
|
||||
@@ -1476,172 +1404,6 @@ describe('Session', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle @path validation error and bubble it to user', async () => {
|
||||
mockConfig.getTargetDir.mockReturnValue('/workspace');
|
||||
(path.resolve as unknown as Mock).mockReturnValue('/tmp/disallowed.txt');
|
||||
mockConfig.validatePathAccess.mockReturnValue('Path is outside workspace');
|
||||
|
||||
// Force fs.stat to fail to skip direct reading and triggers the warning
|
||||
(fs.stat as unknown as Mock).mockRejectedValue(new Error('File not found'));
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: { candidates: [] },
|
||||
},
|
||||
]);
|
||||
mockChat.sendMessageStream.mockResolvedValue(stream);
|
||||
|
||||
await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: 'file://disallowed.txt',
|
||||
mimeType: 'text/plain',
|
||||
name: 'disallowed.txt',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Verify warning sent via sendUpdate
|
||||
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
update: expect.objectContaining({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: expect.objectContaining({
|
||||
text: expect.stringContaining(
|
||||
'Warning: skipping access to `disallowed.txt`. Reason: Path is outside workspace',
|
||||
),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should read absolute file directly if outside workspace', async () => {
|
||||
mockConfig.getTargetDir.mockReturnValue('/workspace');
|
||||
const testFilePath = '/tmp/custom.txt';
|
||||
(path.resolve as unknown as Mock).mockReturnValue(testFilePath);
|
||||
mockConfig.validatePathAccess.mockReturnValue('Path is outside workspace');
|
||||
|
||||
mockConnection.requestPermission.mockResolvedValue({
|
||||
outcome: {
|
||||
outcome: 'selected',
|
||||
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||
},
|
||||
} as unknown as acp.RequestPermissionResponse);
|
||||
|
||||
const mockStats = {
|
||||
isFile: () => true,
|
||||
isDirectory: () => false,
|
||||
};
|
||||
(fs.stat as unknown as Mock).mockResolvedValue(mockStats);
|
||||
(processSingleFileContent as unknown as Mock).mockResolvedValue({
|
||||
llmContent: 'Absolute File Content',
|
||||
});
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: { candidates: [] },
|
||||
},
|
||||
]);
|
||||
mockChat.sendMessageStream.mockResolvedValue(stream);
|
||||
|
||||
await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: `file://${testFilePath}`,
|
||||
mimeType: 'text/plain',
|
||||
name: 'custom.txt',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(processSingleFileContent).toHaveBeenCalledWith(
|
||||
testFilePath,
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
// Verify content appended to sendMessageStream parts
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'Absolute File Content',
|
||||
}),
|
||||
]),
|
||||
expect.anything(),
|
||||
expect.any(AbortSignal),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should read escaping relative file directly if outside workspace', async () => {
|
||||
mockConfig.getTargetDir.mockReturnValue('/workspace');
|
||||
const testFilePath = '../../custom.txt';
|
||||
(path.resolve as unknown as Mock).mockReturnValue('/custom.txt');
|
||||
mockConfig.validatePathAccess.mockReturnValue('Path is outside workspace');
|
||||
|
||||
mockConnection.requestPermission.mockResolvedValue({
|
||||
outcome: {
|
||||
outcome: 'selected',
|
||||
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||
},
|
||||
} as unknown as acp.RequestPermissionResponse);
|
||||
|
||||
const mockStats = {
|
||||
isFile: () => true,
|
||||
isDirectory: () => false,
|
||||
};
|
||||
(fs.stat as unknown as Mock).mockResolvedValue(mockStats);
|
||||
(processSingleFileContent as unknown as Mock).mockResolvedValue({
|
||||
llmContent: 'Escaping Relative File Content',
|
||||
});
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
type: StreamEventType.CHUNK,
|
||||
value: { candidates: [] },
|
||||
},
|
||||
]);
|
||||
mockChat.sendMessageStream.mockResolvedValue(stream);
|
||||
|
||||
await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [
|
||||
{
|
||||
type: 'resource_link',
|
||||
uri: `file://${testFilePath}`,
|
||||
mimeType: 'text/plain',
|
||||
name: 'custom.txt',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(processSingleFileContent).toHaveBeenCalledWith(
|
||||
'/custom.txt',
|
||||
expect.any(String),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
text: 'Escaping Relative File Content',
|
||||
}),
|
||||
]),
|
||||
expect.anything(),
|
||||
expect.any(AbortSignal),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle cancellation during prompt', async () => {
|
||||
let streamController: ReadableStreamDefaultController<unknown>;
|
||||
const stream = new ReadableStream({
|
||||
@@ -1840,6 +1602,7 @@ describe('Session', () => {
|
||||
(fs.stat as unknown as Mock).mockResolvedValue({
|
||||
isDirectory: () => true,
|
||||
});
|
||||
(isWithinRoot as unknown as Mock).mockReturnValue(true);
|
||||
|
||||
const stream = createMockStream([
|
||||
{
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
getDisplayString,
|
||||
processSingleFileContent,
|
||||
type AgentLoopContext,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as acp from '@agentclientprotocol/sdk';
|
||||
@@ -74,17 +73,6 @@ import { runExitCleanup } from '../utils/cleanup.js';
|
||||
import { SessionSelector } from '../utils/sessionUtils.js';
|
||||
|
||||
import { CommandHandler } from './commandHandler.js';
|
||||
|
||||
const RequestPermissionResponseSchema = z.object({
|
||||
outcome: z.discriminatedUnion('outcome', [
|
||||
z.object({ outcome: z.literal('cancelled') }),
|
||||
z.object({
|
||||
outcome: z.literal('selected'),
|
||||
optionId: z.string(),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
export async function runAcpClient(
|
||||
config: Config,
|
||||
settings: LoadedSettings,
|
||||
@@ -110,12 +98,6 @@ export async function runAcpClient(
|
||||
}
|
||||
|
||||
export class GeminiAgent {
|
||||
private static callIdCounter = 0;
|
||||
|
||||
static generateCallId(name: string): string {
|
||||
return `${name}-${Date.now()}-${++GeminiAgent.callIdCounter}`;
|
||||
}
|
||||
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
private clientCapabilities: acp.ClientCapabilities | undefined;
|
||||
private apiKey: string | undefined;
|
||||
@@ -312,7 +294,6 @@ export class GeminiAgent {
|
||||
sessionId,
|
||||
this.clientCapabilities.fs,
|
||||
config.getFileSystemService(),
|
||||
cwd,
|
||||
);
|
||||
config.setFileSystemService(acpFileSystemService);
|
||||
}
|
||||
@@ -370,6 +351,16 @@ export class GeminiAgent {
|
||||
const { sessionData, sessionPath } =
|
||||
await sessionSelector.resolveSession(sessionId);
|
||||
|
||||
if (this.clientCapabilities?.fs) {
|
||||
const acpFileSystemService = new AcpFileSystemService(
|
||||
this.connection,
|
||||
sessionId,
|
||||
this.clientCapabilities.fs,
|
||||
config.getFileSystemService(),
|
||||
);
|
||||
config.setFileSystemService(acpFileSystemService);
|
||||
}
|
||||
|
||||
const clientHistory = convertSessionToClientHistory(sessionData.messages);
|
||||
|
||||
const geminiClient = config.getGeminiClient();
|
||||
@@ -443,19 +434,7 @@ export class GeminiAgent {
|
||||
throw acp.RequestError.authRequired();
|
||||
}
|
||||
|
||||
// 3. Set the ACP FileSystemService (if supported) before config initialization
|
||||
if (this.clientCapabilities?.fs) {
|
||||
const acpFileSystemService = new AcpFileSystemService(
|
||||
this.connection,
|
||||
sessionId,
|
||||
this.clientCapabilities.fs,
|
||||
config.getFileSystemService(),
|
||||
cwd,
|
||||
);
|
||||
config.setFileSystemService(acpFileSystemService);
|
||||
}
|
||||
|
||||
// 4. Now that we are authenticated, it is safe to initialize the config
|
||||
// 3. Now that we are authenticated, it is safe to initialize the config
|
||||
// which starts the MCP servers and other heavy resources.
|
||||
await config.initialize();
|
||||
startupProfiler.flush(config);
|
||||
@@ -918,7 +897,7 @@ export class Session {
|
||||
promptId: string,
|
||||
fc: FunctionCall,
|
||||
): Promise<Part[]> {
|
||||
const callId = fc.id ?? GeminiAgent.generateCallId(fc.name || 'unknown');
|
||||
const callId = fc.id ?? `${fc.name}-${Date.now()}`;
|
||||
const args = fc.args ?? {};
|
||||
|
||||
const startTime = Date.now();
|
||||
@@ -968,23 +947,6 @@ export class Session {
|
||||
try {
|
||||
const invocation = tool.build(args);
|
||||
|
||||
const displayTitle =
|
||||
typeof invocation.getDisplayTitle === 'function'
|
||||
? invocation.getDisplayTitle()
|
||||
: invocation.getDescription();
|
||||
|
||||
const explanation =
|
||||
typeof invocation.getExplanation === 'function'
|
||||
? invocation.getExplanation()
|
||||
: '';
|
||||
|
||||
if (explanation) {
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: { type: 'text', text: explanation },
|
||||
});
|
||||
}
|
||||
|
||||
const confirmationDetails =
|
||||
await invocation.shouldConfirmExecute(abortSignal);
|
||||
|
||||
@@ -1016,19 +978,17 @@ export class Session {
|
||||
toolCall: {
|
||||
toolCallId: callId,
|
||||
status: 'pending',
|
||||
title: displayTitle,
|
||||
title: invocation.getDescription(),
|
||||
content,
|
||||
locations: invocation.toolLocations(),
|
||||
kind: toAcpToolKind(tool.kind),
|
||||
},
|
||||
};
|
||||
|
||||
const output = RequestPermissionResponseSchema.parse(
|
||||
await this.connection.requestPermission(params),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const output = await this.connection.requestPermission(params);
|
||||
const outcome =
|
||||
output.outcome.outcome === 'cancelled'
|
||||
output.outcome.outcome === CoreToolCallStatus.Cancelled
|
||||
? ToolConfirmationOutcome.Cancel
|
||||
: z
|
||||
.nativeEnum(ToolConfirmationOutcome)
|
||||
@@ -1054,14 +1014,12 @@ export class Session {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const content: acp.ToolCallContent[] = [];
|
||||
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'tool_call',
|
||||
toolCallId: callId,
|
||||
status: 'in_progress',
|
||||
title: displayTitle,
|
||||
content,
|
||||
title: invocation.getDescription(),
|
||||
content: [],
|
||||
locations: invocation.toolLocations(),
|
||||
kind: toAcpToolKind(tool.kind),
|
||||
});
|
||||
@@ -1070,14 +1028,12 @@ export class Session {
|
||||
const toolResult: ToolResult = await invocation.execute(abortSignal);
|
||||
const content = toToolCallContent(toolResult);
|
||||
|
||||
const updateContent: acp.ToolCallContent[] = content ? [content] : [];
|
||||
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'tool_call_update',
|
||||
toolCallId: callId,
|
||||
status: 'completed',
|
||||
title: displayTitle,
|
||||
content: updateContent,
|
||||
title: invocation.getDescription(),
|
||||
content: content ? [content] : [],
|
||||
locations: invocation.toolLocations(),
|
||||
kind: toAcpToolKind(tool.kind),
|
||||
});
|
||||
@@ -1239,11 +1195,6 @@ export class Session {
|
||||
const pathSpecsToRead: string[] = [];
|
||||
const contentLabelsForDisplay: string[] = [];
|
||||
const ignoredPaths: string[] = [];
|
||||
const directContents: Array<{
|
||||
spec: string;
|
||||
content?: string;
|
||||
part?: Part;
|
||||
}> = [];
|
||||
|
||||
const toolRegistry = this.context.toolRegistry;
|
||||
const readManyFilesTool = new ReadManyFilesTool(
|
||||
@@ -1266,197 +1217,28 @@ export class Session {
|
||||
}
|
||||
let currentPathSpec = pathName;
|
||||
let resolvedSuccessfully = false;
|
||||
let readDirectly = false;
|
||||
try {
|
||||
const absolutePath = path.resolve(
|
||||
this.context.config.getTargetDir(),
|
||||
pathName,
|
||||
);
|
||||
|
||||
let validationError = this.context.config.validatePathAccess(
|
||||
absolutePath,
|
||||
'read',
|
||||
);
|
||||
|
||||
// We ask the user for explicit permission to read them if outside sandboxed workspace boundaries (and not already authorized).
|
||||
if (
|
||||
validationError &&
|
||||
!isWithinRoot(absolutePath, this.context.config.getTargetDir())
|
||||
) {
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isFile()) {
|
||||
const syntheticCallId = `resolve-prompt-${pathName}-${randomUUID()}`;
|
||||
const params = {
|
||||
sessionId: this.id,
|
||||
options: [
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||
name: 'Allow once',
|
||||
kind: 'allow_once',
|
||||
},
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.Cancel,
|
||||
name: 'Deny',
|
||||
kind: 'reject_once',
|
||||
},
|
||||
] as acp.PermissionOption[],
|
||||
toolCall: {
|
||||
toolCallId: syntheticCallId,
|
||||
status: 'pending',
|
||||
title: `Allow access to absolute path: ${pathName}`,
|
||||
content: [
|
||||
{
|
||||
type: 'content',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `The Agent needs access to read an attached file outside your workspace: ${pathName}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
locations: [],
|
||||
kind: 'read',
|
||||
},
|
||||
};
|
||||
|
||||
const output = RequestPermissionResponseSchema.parse(
|
||||
await this.connection.requestPermission(params),
|
||||
);
|
||||
|
||||
const outcome =
|
||||
output.outcome.outcome === 'cancelled'
|
||||
? ToolConfirmationOutcome.Cancel
|
||||
: z
|
||||
.nativeEnum(ToolConfirmationOutcome)
|
||||
.parse(output.outcome.optionId);
|
||||
|
||||
if (outcome === ToolConfirmationOutcome.ProceedOnce) {
|
||||
this.context.config
|
||||
.getWorkspaceContext()
|
||||
.addReadOnlyPath(absolutePath);
|
||||
validationError = null;
|
||||
} else {
|
||||
this.debug(
|
||||
`Direct read authorization denied for absolute path ${pathName}`,
|
||||
);
|
||||
directContents.push({
|
||||
spec: pathName,
|
||||
content: `[Warning: Access to absolute path \`${pathName}\` denied by user.]`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isWithinRoot(absolutePath, this.context.config.getTargetDir())) {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
currentPathSpec = pathName.endsWith('/')
|
||||
? `${pathName}**`
|
||||
: `${pathName}/**`;
|
||||
this.debug(
|
||||
`Failed to request permission for absolute attachment ${pathName}: ${getErrorMessage(error)}`,
|
||||
`Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`,
|
||||
);
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Warning: Failed to display permission dialog for \`${absolutePath}\`. Error: ${getErrorMessage(error)}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!validationError) {
|
||||
// If it's an absolute path that is authorized (e.g. added via readOnlyPaths),
|
||||
// read it directly to avoid ReadManyFilesTool absolute path resolution issues.
|
||||
if (
|
||||
(path.isAbsolute(pathName) ||
|
||||
!isWithinRoot(
|
||||
absolutePath,
|
||||
this.context.config.getTargetDir(),
|
||||
)) &&
|
||||
!readDirectly
|
||||
) {
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isFile()) {
|
||||
const fileReadResult = await processSingleFileContent(
|
||||
absolutePath,
|
||||
this.context.config.getTargetDir(),
|
||||
this.context.config.getFileSystemService(),
|
||||
);
|
||||
|
||||
if (!fileReadResult.error) {
|
||||
if (
|
||||
typeof fileReadResult.llmContent === 'object' &&
|
||||
'inlineData' in fileReadResult.llmContent
|
||||
) {
|
||||
directContents.push({
|
||||
spec: pathName,
|
||||
part: fileReadResult.llmContent,
|
||||
});
|
||||
} else if (typeof fileReadResult.llmContent === 'string') {
|
||||
let contentToPush = fileReadResult.llmContent;
|
||||
if (fileReadResult.isTruncated) {
|
||||
contentToPush = `[WARNING: This file was truncated]\n\n${contentToPush}`;
|
||||
}
|
||||
directContents.push({
|
||||
spec: pathName,
|
||||
content: contentToPush,
|
||||
});
|
||||
}
|
||||
readDirectly = true;
|
||||
resolvedSuccessfully = true;
|
||||
} else {
|
||||
this.debug(
|
||||
`Direct read failed for absolute path ${pathName}: ${fileReadResult.error}`,
|
||||
);
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Warning: file read failed for \`${pathName}\`. Reason: ${fileReadResult.error}`,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.debug(
|
||||
`File stat/access error for absolute path ${pathName}: ${getErrorMessage(error)}`,
|
||||
);
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Warning: file access failed for \`${pathName}\`. Reason: ${getErrorMessage(error)}`,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!readDirectly) {
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
currentPathSpec = pathName.endsWith('/')
|
||||
? `${pathName}**`
|
||||
: `${pathName}/**`;
|
||||
this.debug(
|
||||
`Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`,
|
||||
);
|
||||
} else {
|
||||
this.debug(
|
||||
`Path ${pathName} resolved to file: ${currentPathSpec}`,
|
||||
);
|
||||
}
|
||||
resolvedSuccessfully = true;
|
||||
} else {
|
||||
this.debug(`Path ${pathName} resolved to file: ${currentPathSpec}`);
|
||||
}
|
||||
resolvedSuccessfully = true;
|
||||
} else {
|
||||
this.debug(
|
||||
`Path ${pathName} access disallowed: ${validationError}. Skipping.`,
|
||||
`Path ${pathName} is outside the project directory. Skipping.`,
|
||||
);
|
||||
await this.sendUpdate({
|
||||
sessionUpdate: 'agent_thought_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Warning: skipping access to \`${pathName}\`. Reason: ${validationError}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
@@ -1516,9 +1298,7 @@ export class Session {
|
||||
}
|
||||
}
|
||||
if (resolvedSuccessfully) {
|
||||
if (!readDirectly) {
|
||||
pathSpecsToRead.push(currentPathSpec);
|
||||
}
|
||||
pathSpecsToRead.push(currentPathSpec);
|
||||
atPathToResolvedSpecMap.set(pathName, currentPathSpec);
|
||||
contentLabelsForDisplay.push(pathName);
|
||||
}
|
||||
@@ -1579,11 +1359,7 @@ export class Session {
|
||||
|
||||
const processedQueryParts: Part[] = [{ text: initialQueryText }];
|
||||
|
||||
if (
|
||||
pathSpecsToRead.length === 0 &&
|
||||
embeddedContext.length === 0 &&
|
||||
directContents.length === 0
|
||||
) {
|
||||
if (pathSpecsToRead.length === 0 && embeddedContext.length === 0) {
|
||||
// Fallback for lone "@" or completely invalid @-commands resulting in empty initialQueryText
|
||||
debugLogger.warn('No valid file paths found in @ commands to read.');
|
||||
return [{ text: initialQueryText }];
|
||||
@@ -1594,7 +1370,7 @@ export class Session {
|
||||
include: pathSpecsToRead,
|
||||
};
|
||||
|
||||
const callId = GeminiAgent.generateCallId(readManyFilesTool.name);
|
||||
const callId = `${readManyFilesTool.name}-${Date.now()}`;
|
||||
|
||||
try {
|
||||
const invocation = readManyFilesTool.build(toolArgs);
|
||||
@@ -1675,30 +1451,6 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
if (directContents.length > 0) {
|
||||
const hasReferenceStart = processedQueryParts.some(
|
||||
(p) =>
|
||||
'text' in p &&
|
||||
typeof p.text === 'string' &&
|
||||
p.text.includes(REFERENCE_CONTENT_START),
|
||||
);
|
||||
if (!hasReferenceStart) {
|
||||
processedQueryParts.push({
|
||||
text: `\n${REFERENCE_CONTENT_START}`,
|
||||
});
|
||||
}
|
||||
for (const item of directContents) {
|
||||
processedQueryParts.push({
|
||||
text: `\nContent from @${item.spec}:\n`,
|
||||
});
|
||||
if (item.content) {
|
||||
processedQueryParts.push({ text: item.content });
|
||||
} else if (item.part) {
|
||||
processedQueryParts.push(item.part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (embeddedContext.length > 0) {
|
||||
processedQueryParts.push({
|
||||
text: '\n--- Content from referenced context ---',
|
||||
@@ -1846,7 +1598,6 @@ function toPermissionOptions(
|
||||
case 'info':
|
||||
case 'ask_user':
|
||||
case 'exit_plan_mode':
|
||||
case 'sandbox_expansion':
|
||||
break;
|
||||
default: {
|
||||
const unreachable: never = confirmation;
|
||||
|
||||
@@ -4,25 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mocked,
|
||||
} from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
||||
import { AcpFileSystemService } from './fileSystemService.js';
|
||||
import type { AgentSideConnection } from '@agentclientprotocol/sdk';
|
||||
import type { FileSystemService } from '@google/gemini-cli-core';
|
||||
import os from 'node:os';
|
||||
|
||||
vi.mock('node:os', () => ({
|
||||
default: {
|
||||
homedir: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AcpFileSystemService', () => {
|
||||
let mockConnection: Mocked<AgentSideConnection>;
|
||||
@@ -40,19 +25,13 @@ describe('AcpFileSystemService', () => {
|
||||
readTextFile: vi.fn(),
|
||||
writeTextFile: vi.fn(),
|
||||
};
|
||||
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('readTextFile', () => {
|
||||
it.each([
|
||||
{
|
||||
capability: true,
|
||||
path: '/path/to/file',
|
||||
desc: 'connection if capability exists and file is inside root',
|
||||
desc: 'connection if capability exists',
|
||||
setup: () => {
|
||||
mockConnection.readTextFile.mockResolvedValue({ content: 'content' });
|
||||
},
|
||||
@@ -66,7 +45,6 @@ describe('AcpFileSystemService', () => {
|
||||
},
|
||||
{
|
||||
capability: false,
|
||||
path: '/path/to/file',
|
||||
desc: 'fallback if capability missing',
|
||||
setup: () => {
|
||||
mockFallback.readTextFile.mockResolvedValue('content');
|
||||
@@ -78,72 +56,19 @@ describe('AcpFileSystemService', () => {
|
||||
expect(mockConnection.readTextFile).not.toHaveBeenCalled();
|
||||
},
|
||||
},
|
||||
{
|
||||
capability: true,
|
||||
path: '/outside/file',
|
||||
desc: 'fallback if capability exists but file is outside root',
|
||||
setup: () => {
|
||||
mockFallback.readTextFile.mockResolvedValue('content');
|
||||
},
|
||||
verify: () => {
|
||||
expect(mockFallback.readTextFile).toHaveBeenCalledWith(
|
||||
'/outside/file',
|
||||
);
|
||||
expect(mockConnection.readTextFile).not.toHaveBeenCalled();
|
||||
},
|
||||
},
|
||||
{
|
||||
capability: true,
|
||||
path: '/home/user/.gemini/tmp/file.md',
|
||||
root: '/home/user',
|
||||
desc: 'fallback if file is inside global gemini dir, even if root overlaps',
|
||||
setup: () => {
|
||||
mockFallback.readTextFile.mockResolvedValue('content');
|
||||
},
|
||||
verify: () => {
|
||||
expect(mockFallback.readTextFile).toHaveBeenCalledWith(
|
||||
'/home/user/.gemini/tmp/file.md',
|
||||
);
|
||||
expect(mockConnection.readTextFile).not.toHaveBeenCalled();
|
||||
},
|
||||
},
|
||||
])(
|
||||
'should use $desc',
|
||||
async ({ capability, path, root, setup, verify }) => {
|
||||
service = new AcpFileSystemService(
|
||||
mockConnection,
|
||||
'session-1',
|
||||
{ readTextFile: capability, writeTextFile: true },
|
||||
mockFallback,
|
||||
root || '/path/to',
|
||||
);
|
||||
setup();
|
||||
|
||||
const result = await service.readTextFile(path);
|
||||
|
||||
expect(result).toBe('content');
|
||||
verify();
|
||||
},
|
||||
);
|
||||
|
||||
it('should throw normalized ENOENT error when readTextFile encounters "Resource not found"', async () => {
|
||||
])('should use $desc', async ({ capability, setup, verify }) => {
|
||||
service = new AcpFileSystemService(
|
||||
mockConnection,
|
||||
'session-1',
|
||||
{ readTextFile: true, writeTextFile: true },
|
||||
{ readTextFile: capability, writeTextFile: true },
|
||||
mockFallback,
|
||||
'/path/to',
|
||||
);
|
||||
mockConnection.readTextFile.mockRejectedValue(
|
||||
new Error('Resource not found for document'),
|
||||
);
|
||||
setup();
|
||||
|
||||
await expect(
|
||||
service.readTextFile('/path/to/missing'),
|
||||
).rejects.toMatchObject({
|
||||
code: 'ENOENT',
|
||||
message: 'Resource not found for document',
|
||||
});
|
||||
const result = await service.readTextFile('/path/to/file');
|
||||
|
||||
expect(result).toBe('content');
|
||||
verify();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,8 +76,7 @@ describe('AcpFileSystemService', () => {
|
||||
it.each([
|
||||
{
|
||||
capability: true,
|
||||
path: '/path/to/file',
|
||||
desc: 'connection if capability exists and file is inside root',
|
||||
desc: 'connection if capability exists',
|
||||
verify: () => {
|
||||
expect(mockConnection.writeTextFile).toHaveBeenCalledWith({
|
||||
path: '/path/to/file',
|
||||
@@ -164,7 +88,6 @@ describe('AcpFileSystemService', () => {
|
||||
},
|
||||
{
|
||||
capability: false,
|
||||
path: '/path/to/file',
|
||||
desc: 'fallback if capability missing',
|
||||
verify: () => {
|
||||
expect(mockFallback.writeTextFile).toHaveBeenCalledWith(
|
||||
@@ -174,63 +97,17 @@ describe('AcpFileSystemService', () => {
|
||||
expect(mockConnection.writeTextFile).not.toHaveBeenCalled();
|
||||
},
|
||||
},
|
||||
{
|
||||
capability: true,
|
||||
path: '/outside/file',
|
||||
desc: 'fallback if capability exists but file is outside root',
|
||||
verify: () => {
|
||||
expect(mockFallback.writeTextFile).toHaveBeenCalledWith(
|
||||
'/outside/file',
|
||||
'content',
|
||||
);
|
||||
expect(mockConnection.writeTextFile).not.toHaveBeenCalled();
|
||||
},
|
||||
},
|
||||
{
|
||||
capability: true,
|
||||
path: '/home/user/.gemini/tmp/file.md',
|
||||
root: '/home/user',
|
||||
desc: 'fallback if file is inside global gemini dir, even if root overlaps',
|
||||
verify: () => {
|
||||
expect(mockFallback.writeTextFile).toHaveBeenCalledWith(
|
||||
'/home/user/.gemini/tmp/file.md',
|
||||
'content',
|
||||
);
|
||||
expect(mockConnection.writeTextFile).not.toHaveBeenCalled();
|
||||
},
|
||||
},
|
||||
])('should use $desc', async ({ capability, path, root, verify }) => {
|
||||
])('should use $desc', async ({ capability, verify }) => {
|
||||
service = new AcpFileSystemService(
|
||||
mockConnection,
|
||||
'session-1',
|
||||
{ writeTextFile: capability, readTextFile: true },
|
||||
mockFallback,
|
||||
root || '/path/to',
|
||||
);
|
||||
|
||||
await service.writeTextFile(path, 'content');
|
||||
await service.writeTextFile('/path/to/file', 'content');
|
||||
|
||||
verify();
|
||||
});
|
||||
|
||||
it('should throw normalized ENOENT error when writeTextFile encounters "Resource not found"', async () => {
|
||||
service = new AcpFileSystemService(
|
||||
mockConnection,
|
||||
'session-1',
|
||||
{ readTextFile: true, writeTextFile: true },
|
||||
mockFallback,
|
||||
'/path/to',
|
||||
);
|
||||
mockConnection.writeTextFile.mockRejectedValue(
|
||||
new Error('Resource not found for directory'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.writeTextFile('/path/to/missing', 'content'),
|
||||
).rejects.toMatchObject({
|
||||
code: 'ENOENT',
|
||||
message: 'Resource not found for directory',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,82 +4,44 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { isWithinRoot, type FileSystemService } from '@google/gemini-cli-core';
|
||||
import type { FileSystemService } from '@google/gemini-cli-core';
|
||||
import type * as acp from '@agentclientprotocol/sdk';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* ACP client-based implementation of FileSystemService
|
||||
*/
|
||||
export class AcpFileSystemService implements FileSystemService {
|
||||
private readonly geminiDir = path.join(os.homedir(), '.gemini');
|
||||
|
||||
constructor(
|
||||
private readonly connection: acp.AgentSideConnection,
|
||||
private readonly sessionId: string,
|
||||
private readonly capabilities: acp.FileSystemCapabilities,
|
||||
private readonly fallback: FileSystemService,
|
||||
private readonly root: string,
|
||||
) {}
|
||||
|
||||
private shouldUseFallback(filePath: string): boolean {
|
||||
// Files inside the global CLI directory must always use the native file system,
|
||||
// even if the user runs the CLI directly from their home directory (which
|
||||
// would make the IDE's project root overlap with the global directory).
|
||||
return (
|
||||
!isWithinRoot(filePath, this.root) ||
|
||||
isWithinRoot(filePath, this.geminiDir)
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeFileSystemError(err: unknown): never {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
if (
|
||||
errorMessage.includes('Resource not found') ||
|
||||
errorMessage.includes('ENOENT') ||
|
||||
errorMessage.includes('does not exist') ||
|
||||
errorMessage.includes('No such file')
|
||||
) {
|
||||
const newErr = new Error(errorMessage) as NodeJS.ErrnoException;
|
||||
newErr.code = 'ENOENT';
|
||||
throw newErr;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
async readTextFile(filePath: string): Promise<string> {
|
||||
if (!this.capabilities.readTextFile || this.shouldUseFallback(filePath)) {
|
||||
if (!this.capabilities.readTextFile) {
|
||||
return this.fallback.readTextFile(filePath);
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const response = await this.connection.readTextFile({
|
||||
path: filePath,
|
||||
sessionId: this.sessionId,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const response = await this.connection.readTextFile({
|
||||
path: filePath,
|
||||
sessionId: this.sessionId,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return response.content;
|
||||
} catch (err: unknown) {
|
||||
this.normalizeFileSystemError(err);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return response.content;
|
||||
}
|
||||
|
||||
async writeTextFile(filePath: string, content: string): Promise<void> {
|
||||
if (!this.capabilities.writeTextFile || this.shouldUseFallback(filePath)) {
|
||||
if (!this.capabilities.writeTextFile) {
|
||||
return this.fallback.writeTextFile(filePath, content);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.writeTextFile({
|
||||
path: filePath,
|
||||
content,
|
||||
sessionId: this.sessionId,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
this.normalizeFileSystemError(err);
|
||||
}
|
||||
await this.connection.writeTextFile({
|
||||
path: filePath,
|
||||
content,
|
||||
sessionId: this.sessionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ toolName = "grep_search"
|
||||
argsPattern = "(\.env|id_rsa|passwd)"
|
||||
decision = "deny"
|
||||
priority = 200
|
||||
denyMessage = "Access to sensitive credentials or system files is restricted by the policy-example extension."
|
||||
deny_message = "Access to sensitive credentials or system files is restricted by the policy-example extension."
|
||||
|
||||
# Safety Checker: Apply path validation to all write operations.
|
||||
[[safety_checker]]
|
||||
|
||||
@@ -322,41 +322,6 @@ describe('parseArguments', () => {
|
||||
},
|
||||
);
|
||||
|
||||
describe('isCommand middleware', () => {
|
||||
it.each([
|
||||
{ cmd: 'mcp list', expected: true },
|
||||
{ cmd: 'extensions list', expected: true },
|
||||
{ cmd: 'extension list', expected: true },
|
||||
{ cmd: 'skills list', expected: true },
|
||||
{ cmd: 'skill list', expected: true },
|
||||
{ cmd: 'hooks migrate', expected: true },
|
||||
{ cmd: 'hook migrate', expected: true },
|
||||
{ cmd: 'some query', expected: undefined },
|
||||
{ cmd: 'hello world', expected: undefined },
|
||||
])(
|
||||
'should set isCommand to $expected for "$cmd"',
|
||||
async ({ cmd, expected }) => {
|
||||
process.argv = ['node', 'script.js', ...cmd.split(' ')];
|
||||
const settings = createTestMergedSettings({
|
||||
admin: {
|
||||
mcp: { enabled: true },
|
||||
},
|
||||
experimental: {
|
||||
extensionManagement: true,
|
||||
},
|
||||
skills: {
|
||||
enabled: true,
|
||||
},
|
||||
hooksConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
const parsedArgs = await parseArguments(settings);
|
||||
expect(parsedArgs.isCommand).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description: 'should allow --prompt without --prompt-interactive',
|
||||
|
||||
@@ -163,104 +163,12 @@ export async function parseArguments(
|
||||
.usage(
|
||||
'Usage: gemini [options] [command]\n\nGemini CLI - Defaults to interactive mode. Use -p/--prompt for non-interactive (headless) mode.',
|
||||
)
|
||||
.option('isCommand', {
|
||||
type: 'boolean',
|
||||
hidden: true,
|
||||
description: 'Internal flag to indicate if a subcommand is being run',
|
||||
})
|
||||
.option('debug', {
|
||||
alias: 'd',
|
||||
type: 'boolean',
|
||||
description: 'Run in debug mode (open debug console with F12)',
|
||||
default: false,
|
||||
})
|
||||
.middleware((argv) => {
|
||||
const commandModules = [
|
||||
mcpCommand,
|
||||
extensionsCommand,
|
||||
skillsCommand,
|
||||
hooksCommand,
|
||||
];
|
||||
|
||||
const subcommands = commandModules.flatMap((mod) => {
|
||||
const names: string[] = [];
|
||||
|
||||
const cmd = mod.command;
|
||||
if (cmd) {
|
||||
if (Array.isArray(cmd)) {
|
||||
for (const c of cmd) {
|
||||
names.push(String(c).split(' ')[0]);
|
||||
}
|
||||
} else {
|
||||
names.push(String(cmd).split(' ')[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const aliases = mod.aliases;
|
||||
if (aliases) {
|
||||
if (Array.isArray(aliases)) {
|
||||
for (const a of aliases) {
|
||||
names.push(String(a).split(' ')[0]);
|
||||
}
|
||||
} else {
|
||||
names.push(String(aliases).split(' ')[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
});
|
||||
|
||||
const firstArg = argv._[0];
|
||||
if (typeof firstArg === 'string' && subcommands.includes(firstArg)) {
|
||||
argv['isCommand'] = true;
|
||||
}
|
||||
}, true)
|
||||
// Ensure validation flows through .fail() for clean UX
|
||||
.fail((msg, err) => {
|
||||
if (err) throw err;
|
||||
throw new Error(msg);
|
||||
})
|
||||
.check((argv) => {
|
||||
// The 'query' positional can be a string (for one arg) or string[] (for multiple).
|
||||
// This guard safely checks if any positional argument was provided.
|
||||
const queryArg = argv['query'];
|
||||
const query =
|
||||
typeof queryArg === 'string' || Array.isArray(queryArg)
|
||||
? queryArg
|
||||
: undefined;
|
||||
const hasPositionalQuery = Array.isArray(query)
|
||||
? query.length > 0
|
||||
: !!query;
|
||||
|
||||
if (argv['prompt'] && hasPositionalQuery) {
|
||||
return 'Cannot use both a positional prompt and the --prompt (-p) flag together';
|
||||
}
|
||||
if (argv['prompt'] && argv['promptInteractive']) {
|
||||
return 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together';
|
||||
}
|
||||
if (argv['yolo'] && argv['approvalMode']) {
|
||||
return 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.';
|
||||
}
|
||||
|
||||
const outputFormat = argv['outputFormat'];
|
||||
if (
|
||||
typeof outputFormat === 'string' &&
|
||||
!['text', 'json', 'stream-json'].includes(outputFormat)
|
||||
) {
|
||||
return `Invalid values:\n Argument: output-format, Given: "${outputFormat}", Choices: "text", "json", "stream-json"`;
|
||||
}
|
||||
if (argv['worktree'] && !settings.experimental?.worktrees) {
|
||||
return 'The --worktree flag is only available when experimental.worktrees is enabled in your settings.';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
yargsInstance.command(mcpCommand);
|
||||
yargsInstance.command(extensionsCommand);
|
||||
yargsInstance.command(skillsCommand);
|
||||
yargsInstance.command(hooksCommand);
|
||||
|
||||
yargsInstance
|
||||
.command('$0 [query..]', 'Launch Gemini CLI', (yargsInstance) =>
|
||||
yargsInstance
|
||||
.positional('query', {
|
||||
@@ -444,6 +352,59 @@ export async function parseArguments(
|
||||
description: 'Suppress the security warning when using --raw-output.',
|
||||
}),
|
||||
)
|
||||
// Register MCP subcommands
|
||||
.command(mcpCommand)
|
||||
// Ensure validation flows through .fail() for clean UX
|
||||
.fail((msg, err) => {
|
||||
if (err) throw err;
|
||||
throw new Error(msg);
|
||||
})
|
||||
.check((argv) => {
|
||||
// The 'query' positional can be a string (for one arg) or string[] (for multiple).
|
||||
// This guard safely checks if any positional argument was provided.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const query = argv['query'] as string | string[] | undefined;
|
||||
const hasPositionalQuery = Array.isArray(query)
|
||||
? query.length > 0
|
||||
: !!query;
|
||||
|
||||
if (argv['prompt'] && hasPositionalQuery) {
|
||||
return 'Cannot use both a positional prompt and the --prompt (-p) flag together';
|
||||
}
|
||||
if (argv['prompt'] && argv['promptInteractive']) {
|
||||
return 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together';
|
||||
}
|
||||
if (argv['yolo'] && argv['approvalMode']) {
|
||||
return 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.';
|
||||
}
|
||||
if (
|
||||
argv['outputFormat'] &&
|
||||
!['text', 'json', 'stream-json'].includes(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
argv['outputFormat'] as string,
|
||||
)
|
||||
) {
|
||||
return `Invalid values:\n Argument: output-format, Given: "${argv['outputFormat']}", Choices: "text", "json", "stream-json"`;
|
||||
}
|
||||
if (argv['worktree'] && !settings.experimental?.worktrees) {
|
||||
return 'The --worktree flag is only available when experimental.worktrees is enabled in your settings.';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (settings.experimental?.extensionManagement) {
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
if (settings.skills?.enabled ?? true) {
|
||||
yargsInstance.command(skillsCommand);
|
||||
}
|
||||
// Register hooks command if hooks are enabled
|
||||
if (settings.hooksConfig.enabled) {
|
||||
yargsInstance.command(hooksCommand);
|
||||
}
|
||||
|
||||
yargsInstance
|
||||
.version(await getVersion()) // This will enable the --version flag based on package.json
|
||||
.alias('v', 'version')
|
||||
.help()
|
||||
|
||||
@@ -614,7 +614,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
this.loadingPromise = (async () => {
|
||||
try {
|
||||
if (this.settings.admin?.extensions?.enabled === false) {
|
||||
if (this.settings.admin.extensions.enabled === false) {
|
||||
this.loadedExtensions = [];
|
||||
return this.loadedExtensions;
|
||||
}
|
||||
@@ -824,11 +824,11 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
|
||||
if (config.mcpServers) {
|
||||
if (this.settings.admin?.mcp?.enabled === false) {
|
||||
if (this.settings.admin.mcp.enabled === false) {
|
||||
config.mcpServers = undefined;
|
||||
} else {
|
||||
// Apply admin allowlist if configured
|
||||
const adminAllowlist = this.settings.admin?.mcp?.config;
|
||||
const adminAllowlist = this.settings.admin.mcp.config;
|
||||
if (adminAllowlist && Object.keys(adminAllowlist).length > 0) {
|
||||
const result = applyAdminAllowlist(
|
||||
config.mcpServers,
|
||||
@@ -1298,9 +1298,7 @@ export async function inferInstallMetadata(
|
||||
source.startsWith('http://') ||
|
||||
source.startsWith('https://') ||
|
||||
source.startsWith('git@') ||
|
||||
source.startsWith('sso://') ||
|
||||
source.startsWith('github:') ||
|
||||
source.startsWith('gitlab:')
|
||||
source.startsWith('sso://')
|
||||
) {
|
||||
return {
|
||||
source,
|
||||
|
||||
@@ -381,7 +381,6 @@ describe('Policy Engine Integration Tests', () => {
|
||||
// Add a manual rule with annotations to the config
|
||||
config.rules = config.rules || [];
|
||||
config.rules.push({
|
||||
toolName: '*',
|
||||
toolAnnotations: { readOnlyHint: true },
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 10,
|
||||
|
||||
@@ -300,7 +300,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description:
|
||||
'The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode.',
|
||||
'The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory.',
|
||||
showInDialog: true,
|
||||
},
|
||||
modelRouting: {
|
||||
@@ -657,16 +657,6 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Hide the footer from the UI',
|
||||
showInDialog: true,
|
||||
},
|
||||
collapseDrawerDuringApproval: {
|
||||
type: 'boolean',
|
||||
label: 'Collapse Drawer During Approval',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description:
|
||||
'Whether to collapse the UI drawer when a tool is awaiting confirmation.',
|
||||
showInDialog: false,
|
||||
},
|
||||
showMemoryUsage: {
|
||||
type: 'boolean',
|
||||
label: 'Show Memory Usage',
|
||||
@@ -1208,16 +1198,6 @@ const SETTINGS_SCHEMA = {
|
||||
'Disable user input on browser window during automation.',
|
||||
showInDialog: false,
|
||||
},
|
||||
maxActionsPerTask: {
|
||||
type: 'number',
|
||||
label: 'Max Actions Per Task',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: 100,
|
||||
description:
|
||||
'The maximum number of tool calls allowed per browser task. Enforcement is hard: the agent will be terminated when the limit is reached.',
|
||||
showInDialog: false,
|
||||
},
|
||||
confirmSensitiveActions: {
|
||||
type: 'boolean',
|
||||
label: 'Confirm Sensitive Actions',
|
||||
@@ -3024,7 +3004,6 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
type: 'object',
|
||||
properties: {
|
||||
useGemini3_1: { type: 'boolean' },
|
||||
useGemini3_1FlashLite: { type: 'boolean' },
|
||||
useCustomTools: { type: 'boolean' },
|
||||
hasAccessToPreview: { type: 'boolean' },
|
||||
requestedModels: {
|
||||
|
||||
@@ -105,9 +105,6 @@ describe('initializer', () => {
|
||||
mockSettings,
|
||||
);
|
||||
|
||||
// Wait for the background promise to resolve
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(result).toEqual({
|
||||
authError: null,
|
||||
accountSuspensionInfo: null,
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
StartSessionEvent,
|
||||
logCliConfiguration,
|
||||
startupProfiler,
|
||||
debugLogger,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type LoadedSettings } from '../config/settings.js';
|
||||
import { performInitialAuth } from './auth.js';
|
||||
@@ -56,18 +55,9 @@ export async function initializeApp(
|
||||
);
|
||||
|
||||
if (config.getIdeMode()) {
|
||||
IdeClient.getInstance()
|
||||
.then(async (ideClient) => {
|
||||
await ideClient.connect();
|
||||
logIdeConnection(
|
||||
config,
|
||||
new IdeConnectionEvent(IdeConnectionType.START),
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
// We log locally if IDE connection setup fails in the background.
|
||||
debugLogger.error('Failed to initialize IDE client:', e);
|
||||
});
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
await ideClient.connect();
|
||||
logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -528,62 +528,6 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should call process.stdin.resume when isInteractive is true to protect against implicit Node pause', async () => {
|
||||
const resumeSpy = vi.spyOn(process.stdin, 'resume');
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(
|
||||
createMockConfig({
|
||||
isInteractive: () => true,
|
||||
getQuestion: () => '',
|
||||
getSandbox: () => undefined,
|
||||
}),
|
||||
);
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
ui: {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
model: undefined,
|
||||
sandbox: undefined,
|
||||
debug: undefined,
|
||||
prompt: undefined,
|
||||
promptInteractive: undefined,
|
||||
query: undefined,
|
||||
yolo: undefined,
|
||||
approvalMode: undefined,
|
||||
policy: undefined,
|
||||
adminPolicy: undefined,
|
||||
allowedMcpServerNames: undefined,
|
||||
allowedTools: undefined,
|
||||
experimentalAcp: undefined,
|
||||
extensions: undefined,
|
||||
listExtensions: undefined,
|
||||
includeDirectories: undefined,
|
||||
screenReader: undefined,
|
||||
useWriteTodos: undefined,
|
||||
resume: undefined,
|
||||
listSessions: undefined,
|
||||
deleteSession: undefined,
|
||||
outputFormat: undefined,
|
||||
fakeResponses: undefined,
|
||||
recordResponses: undefined,
|
||||
rawOutput: undefined,
|
||||
acceptRawOutputRisk: undefined,
|
||||
isCommand: undefined,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await main();
|
||||
});
|
||||
|
||||
expect(resumeSpy).toHaveBeenCalledTimes(1);
|
||||
resumeSpy.mockRestore();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ flag: 'listExtensions' },
|
||||
{ flag: 'listSessions' },
|
||||
|
||||
@@ -336,7 +336,7 @@ export async function main() {
|
||||
// the sandbox because the sandbox will interfere with the Oauth2 web
|
||||
// redirect.
|
||||
let initialAuthFailed = false;
|
||||
if (!settings.merged.security.auth.useExternal && !argv.isCommand) {
|
||||
if (!settings.merged.security.auth.useExternal) {
|
||||
try {
|
||||
if (
|
||||
partialConfig.isInteractive() &&
|
||||
@@ -388,7 +388,7 @@ export async function main() {
|
||||
await runDeferredCommand(settings.merged);
|
||||
|
||||
// hop into sandbox if we are outside and sandboxing is enabled
|
||||
if (!process.env['SANDBOX'] && !argv.isCommand) {
|
||||
if (!process.env['SANDBOX']) {
|
||||
const memoryArgs = settings.merged.advanced.autoConfigureMemory
|
||||
? getNodeMemoryArgs(isDebugMode)
|
||||
: [];
|
||||
@@ -613,17 +613,8 @@ export async function main() {
|
||||
}
|
||||
|
||||
cliStartupHandle?.end();
|
||||
|
||||
// Render UI, passing necessary config values. Check that there is no command line question.
|
||||
if (config.isInteractive()) {
|
||||
// Earlier initialization phases (like TerminalCapabilityManager resolving
|
||||
// or authWithWeb) may have added and removed 'data' listeners on process.stdin.
|
||||
// When the listener count drops to 0, Node.js implicitly pauses the stream buffer.
|
||||
// React Ink's useInput hooks will silently fail to receive keystrokes if the stream remains paused.
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.resume();
|
||||
}
|
||||
|
||||
await startInteractiveUI(
|
||||
config,
|
||||
settings,
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('SlashCommandResolver', () => {
|
||||
]);
|
||||
|
||||
expect(finalCommands.map((c) => c.name)).toContain('deploy');
|
||||
expect(finalCommands.map((c) => c.name)).toContain('firebase:deploy');
|
||||
expect(finalCommands.map((c) => c.name)).toContain('firebase.deploy');
|
||||
expect(conflicts).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('SlashCommandResolver', () => {
|
||||
|
||||
it('should apply numeric suffixes when renames also conflict', () => {
|
||||
const user1 = createMockCommand('deploy', CommandKind.USER_FILE);
|
||||
const user2 = createMockCommand('gcp:deploy', CommandKind.USER_FILE);
|
||||
const user2 = createMockCommand('gcp.deploy', CommandKind.USER_FILE);
|
||||
const extension = {
|
||||
...createMockCommand('deploy', CommandKind.EXTENSION_FILE),
|
||||
extensionName: 'gcp',
|
||||
@@ -171,7 +171,7 @@ describe('SlashCommandResolver', () => {
|
||||
extension,
|
||||
]);
|
||||
|
||||
expect(finalCommands.find((c) => c.name === 'gcp:deploy1')).toBeDefined();
|
||||
expect(finalCommands.find((c) => c.name === 'gcp.deploy1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should prefix skills with extension name when they conflict with built-in', () => {
|
||||
@@ -185,37 +185,7 @@ describe('SlashCommandResolver', () => {
|
||||
|
||||
const names = finalCommands.map((c) => c.name);
|
||||
expect(names).toContain('chat');
|
||||
expect(names).toContain('google-workspace:chat');
|
||||
});
|
||||
|
||||
it('should ALWAYS prefix extension skills even if no conflict exists', () => {
|
||||
const skill = {
|
||||
...createMockCommand('chat', CommandKind.SKILL),
|
||||
extensionName: 'google-workspace',
|
||||
};
|
||||
|
||||
const { finalCommands } = SlashCommandResolver.resolve([skill]);
|
||||
|
||||
const names = finalCommands.map((c) => c.name);
|
||||
expect(names).toContain('google-workspace:chat');
|
||||
expect(names).not.toContain('chat');
|
||||
});
|
||||
|
||||
it('should use numeric suffixes if prefixed skill names collide', () => {
|
||||
const skill1 = {
|
||||
...createMockCommand('chat', CommandKind.SKILL),
|
||||
extensionName: 'google-workspace',
|
||||
};
|
||||
const skill2 = {
|
||||
...createMockCommand('chat', CommandKind.SKILL),
|
||||
extensionName: 'google-workspace',
|
||||
};
|
||||
|
||||
const { finalCommands } = SlashCommandResolver.resolve([skill1, skill2]);
|
||||
|
||||
const names = finalCommands.map((c) => c.name);
|
||||
expect(names).toContain('google-workspace:chat');
|
||||
expect(names).toContain('google-workspace:chat1');
|
||||
expect(names).toContain('google-workspace.chat');
|
||||
});
|
||||
|
||||
it('should NOT prefix skills with "skill" when extension name is missing', () => {
|
||||
|
||||
@@ -47,17 +47,7 @@ export class SlashCommandResolver {
|
||||
const originalName = cmd.name;
|
||||
let finalName = originalName;
|
||||
|
||||
const shouldAlwaysPrefix =
|
||||
cmd.kind === CommandKind.SKILL && !!cmd.extensionName;
|
||||
|
||||
if (shouldAlwaysPrefix) {
|
||||
finalName = this.getRenamedName(
|
||||
originalName,
|
||||
this.getPrefix(cmd),
|
||||
registry.commandMap,
|
||||
cmd.kind,
|
||||
);
|
||||
} else if (registry.firstEncounters.has(originalName)) {
|
||||
if (registry.firstEncounters.has(originalName)) {
|
||||
// We've already seen a command with this name, so resolve the conflict.
|
||||
finalName = this.handleConflict(cmd, registry);
|
||||
} else {
|
||||
@@ -103,7 +93,6 @@ export class SlashCommandResolver {
|
||||
incoming.name,
|
||||
this.getPrefix(incoming),
|
||||
registry.commandMap,
|
||||
incoming.kind,
|
||||
);
|
||||
this.trackConflict(
|
||||
registry.conflictsMap,
|
||||
@@ -143,7 +132,6 @@ export class SlashCommandResolver {
|
||||
currentOwner.name,
|
||||
this.getPrefix(currentOwner),
|
||||
registry.commandMap,
|
||||
currentOwner.kind,
|
||||
);
|
||||
|
||||
// Update the registry: remove the old name and add the owner under the new name.
|
||||
@@ -168,12 +156,8 @@ export class SlashCommandResolver {
|
||||
name: string,
|
||||
prefix: string | undefined,
|
||||
commandMap: Map<string, SlashCommand>,
|
||||
kind?: CommandKind,
|
||||
): string {
|
||||
const isExtensionPrefix =
|
||||
kind === CommandKind.SKILL || kind === CommandKind.EXTENSION_FILE;
|
||||
const separator = isExtensionPrefix ? ':' : '.';
|
||||
const base = prefix ? `${prefix}${separator}${name}` : name;
|
||||
const base = prefix ? `${prefix}.${name}` : name;
|
||||
let renamedName = base;
|
||||
let suffix = 1;
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ export class AppRig {
|
||||
private sessionId: string;
|
||||
|
||||
private pendingConfirmations = new Map<string, PendingConfirmation>();
|
||||
private breakpointTools = new Set<string>();
|
||||
private breakpointTools = new Set<string | undefined>();
|
||||
private lastAwaitedConfirmation: PendingConfirmation | undefined;
|
||||
|
||||
/**
|
||||
@@ -181,16 +181,6 @@ export class AppRig {
|
||||
);
|
||||
this.sessionId = `test-session-${uniqueId}`;
|
||||
activeRigs.set(this.sessionId, this);
|
||||
|
||||
// Pre-create the persistent state file to bypass the terminal setup prompt
|
||||
const geminiDir = path.join(this.testDir, '.gemini');
|
||||
if (!fs.existsSync(geminiDir)) {
|
||||
fs.mkdirSync(geminiDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(geminiDir, 'state.json'),
|
||||
JSON.stringify({ terminalSetupPromptShown: true }),
|
||||
);
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
@@ -446,7 +436,11 @@ export class AppRig {
|
||||
MockShellExecutionService.setMockCommands(commands);
|
||||
}
|
||||
|
||||
setToolPolicy(toolName: string, decision: PolicyDecision, priority = 10) {
|
||||
setToolPolicy(
|
||||
toolName: string | undefined,
|
||||
decision: PolicyDecision,
|
||||
priority = 10,
|
||||
) {
|
||||
if (!this.config) throw new Error('AppRig not initialized');
|
||||
this.config.getPolicyEngine().addRule({
|
||||
toolName,
|
||||
@@ -456,20 +450,27 @@ export class AppRig {
|
||||
});
|
||||
}
|
||||
|
||||
setBreakpoint(toolName: string | string[]) {
|
||||
setBreakpoint(toolName: string | string[] | undefined) {
|
||||
if (Array.isArray(toolName)) {
|
||||
for (const name of toolName) {
|
||||
this.setBreakpoint(name);
|
||||
}
|
||||
} else {
|
||||
this.setToolPolicy(toolName, PolicyDecision.ASK_USER, 100);
|
||||
// Use undefined toolName to create a global rule if '*' is provided
|
||||
const actualToolName = toolName === '*' ? undefined : toolName;
|
||||
this.setToolPolicy(actualToolName, PolicyDecision.ASK_USER, 100);
|
||||
this.breakpointTools.add(toolName);
|
||||
}
|
||||
}
|
||||
|
||||
removeToolPolicy(toolName: string, source = 'AppRig Override') {
|
||||
removeToolPolicy(toolName?: string, source = 'AppRig Override') {
|
||||
if (!this.config) throw new Error('AppRig not initialized');
|
||||
this.config.getPolicyEngine().removeRulesForTool(toolName, source);
|
||||
// Map '*' back to undefined for policy removal
|
||||
const actualToolName = toolName === '*' ? undefined : toolName;
|
||||
this.config
|
||||
.getPolicyEngine()
|
||||
|
||||
.removeRulesForTool(actualToolName as string, source);
|
||||
this.breakpointTools.delete(toolName);
|
||||
}
|
||||
|
||||
|
||||
@@ -524,8 +524,6 @@ const baseMockUiState = {
|
||||
nightly: false,
|
||||
updateInfo: null,
|
||||
pendingHistoryItems: [],
|
||||
mainControlsRef: () => {},
|
||||
rootUiRef: { current: null },
|
||||
};
|
||||
|
||||
export const mockAppState: AppState = {
|
||||
@@ -667,7 +665,7 @@ export const renderWithProviders = async (
|
||||
);
|
||||
}
|
||||
|
||||
const mainAreaWidth = providedUiState?.mainAreaWidth ?? terminalWidth;
|
||||
const mainAreaWidth = terminalWidth;
|
||||
|
||||
const finalUiState = {
|
||||
...baseState,
|
||||
|
||||
@@ -70,7 +70,9 @@ describe('App', () => {
|
||||
cleanUiDetailsVisible: true,
|
||||
quittingMessages: null,
|
||||
dialogsVisible: false,
|
||||
mainControlsRef: vi.fn(),
|
||||
mainControlsRef: {
|
||||
current: null,
|
||||
} as unknown as React.MutableRefObject<DOMElement | null>,
|
||||
rootUiRef: {
|
||||
current: null,
|
||||
} as unknown as React.MutableRefObject<DOMElement | null>,
|
||||
|
||||
@@ -489,8 +489,8 @@ describe('AppContainer State Management', () => {
|
||||
// Mock LoadedSettings
|
||||
mockSettings = createMockSettings({
|
||||
hideBanner: false,
|
||||
hideTips: false,
|
||||
hideFooter: false,
|
||||
hideTips: false,
|
||||
showMemoryUsage: false,
|
||||
theme: 'default',
|
||||
ui: {
|
||||
@@ -911,8 +911,8 @@ describe('AppContainer State Management', () => {
|
||||
it('handles settings with all display options disabled', async () => {
|
||||
const settingsAllHidden = createMockSettings({
|
||||
hideBanner: true,
|
||||
hideTips: true,
|
||||
hideFooter: true,
|
||||
hideTips: true,
|
||||
showMemoryUsage: false,
|
||||
});
|
||||
|
||||
@@ -2157,8 +2157,13 @@ describe('AppContainer State Management', () => {
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
|
||||
pressKey('\x04'); // Ctrl+D
|
||||
// It should still not quit because buffer is non-empty.
|
||||
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
|
||||
// Now count is 2, it should quit.
|
||||
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
|
||||
'/quit',
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from 'react';
|
||||
import {
|
||||
type DOMElement,
|
||||
ResizeObserver,
|
||||
measureElement,
|
||||
useApp,
|
||||
useStdout,
|
||||
useStdin,
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
import { ConfigContext } from './contexts/ConfigContext.js';
|
||||
import {
|
||||
type HistoryItem,
|
||||
type HistoryItemWithoutId,
|
||||
type HistoryItemToolGroup,
|
||||
AuthState,
|
||||
type ConfirmationRequest,
|
||||
type PermissionConfirmationRequest,
|
||||
@@ -79,6 +81,7 @@ import {
|
||||
type AgentsDiscoveredPayload,
|
||||
ChangeAuthRequestedError,
|
||||
ProjectIdRequiredError,
|
||||
CoreToolCallStatus,
|
||||
buildUserSteeringHintPrompt,
|
||||
logBillingEvent,
|
||||
ApiKeyUpdatedEvent,
|
||||
@@ -167,11 +170,29 @@ import { useIsHelpDismissKey } from './utils/shortcutsHelp.js';
|
||||
import { useSuspend } from './hooks/useSuspend.js';
|
||||
import { useRunEventNotifications } from './hooks/useRunEventNotifications.js';
|
||||
import { isNotificationsEnabled } from '../utils/terminalNotifications.js';
|
||||
import {
|
||||
isToolExecuting,
|
||||
isToolAwaitingConfirmation,
|
||||
getAllToolCalls,
|
||||
} from './utils/historyUtils.js';
|
||||
|
||||
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
|
||||
return pendingHistoryItems.some((item) => {
|
||||
if (item && item.type === 'tool_group') {
|
||||
return item.tools.some(
|
||||
(tool) => CoreToolCallStatus.Executing === tool.status,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function isToolAwaitingConfirmation(
|
||||
pendingHistoryItems: HistoryItemWithoutId[],
|
||||
) {
|
||||
return pendingHistoryItems
|
||||
.filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
|
||||
.some((item) =>
|
||||
item.tools.some(
|
||||
(tool) => CoreToolCallStatus.AwaitingApproval === tool.status,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
interface AppContainerProps {
|
||||
config: Config;
|
||||
@@ -397,6 +418,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const branchName = useGitBranchName(config.getTargetDir());
|
||||
|
||||
// Layout measurements
|
||||
const mainControlsRef = useRef<DOMElement>(null);
|
||||
// For performance profiling only
|
||||
const rootUiRef = useRef<DOMElement>(null);
|
||||
const lastTitleRef = useRef<string | null>(null);
|
||||
@@ -699,10 +721,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
|
||||
// Derive auth state variables for backward compatibility with UIStateContext
|
||||
const isAuthDialogOpen = authState === AuthState.Updating;
|
||||
// TODO: Consider handling other auth types that should also skip the blocking screen
|
||||
const isAuthenticating =
|
||||
authState === AuthState.Unauthenticated &&
|
||||
settings.merged.security.auth.selectedType !== AuthType.USE_GEMINI;
|
||||
const isAuthenticating = authState === AuthState.Unauthenticated;
|
||||
|
||||
// Session browser and resume functionality
|
||||
const isGeminiClientInitialized = config.getGeminiClient()?.isInitialized();
|
||||
@@ -1132,16 +1151,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
consumePendingHints,
|
||||
);
|
||||
|
||||
const pendingHistoryItems = useMemo(
|
||||
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
|
||||
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
|
||||
);
|
||||
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() => isToolAwaitingConfirmation(pendingHistoryItems),
|
||||
[pendingHistoryItems],
|
||||
);
|
||||
|
||||
toggleBackgroundShellRef.current = toggleBackgroundShell;
|
||||
isBackgroundShellVisibleRef.current = isBackgroundShellVisible;
|
||||
backgroundShellsRef.current = backgroundShells;
|
||||
@@ -1213,6 +1222,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
cancelHandlerRef.current = useCallback(
|
||||
(shouldRestorePrompt: boolean = true) => {
|
||||
const pendingHistoryItems = [
|
||||
...pendingSlashCommandHistoryItems,
|
||||
...pendingGeminiHistoryItems,
|
||||
];
|
||||
if (isToolAwaitingConfirmation(pendingHistoryItems)) {
|
||||
return; // Don't clear - user may be composing a follow-up message
|
||||
}
|
||||
@@ -1246,7 +1259,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
inputHistory,
|
||||
getQueuedMessagesText,
|
||||
clearQueue,
|
||||
pendingHistoryItems,
|
||||
pendingSlashCommandHistoryItems,
|
||||
pendingGeminiHistoryItems,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1282,7 +1296,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
const isIdle = streamingState === StreamingState.Idle;
|
||||
const isAgentRunning =
|
||||
streamingState === StreamingState.Responding ||
|
||||
isToolExecuting(pendingHistoryItems);
|
||||
isToolExecuting([
|
||||
...pendingSlashCommandHistoryItems,
|
||||
...pendingGeminiHistoryItems,
|
||||
]);
|
||||
|
||||
if (isSlash && isAgentRunning) {
|
||||
const { commandToExecute } = parseSlashCommand(
|
||||
@@ -1302,8 +1319,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
return;
|
||||
}
|
||||
|
||||
const isMcpOrConfigReady = isConfigInitialized && isMcpReady;
|
||||
if ((isSlash && isConfigInitialized) || (isIdle && isMcpOrConfigReady)) {
|
||||
if (isSlash || (isIdle && isMcpReady)) {
|
||||
if (!isSlash) {
|
||||
const permissions = await checkPermissions(submittedValue, config);
|
||||
if (permissions.length > 0) {
|
||||
@@ -1326,12 +1342,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
void submitQuery(submittedValue);
|
||||
} else {
|
||||
// Check messageQueue.length === 0 to only notify on the first queued item
|
||||
if (isIdle && !isMcpOrConfigReady && messageQueue.length === 0) {
|
||||
if (isIdle && !isMcpReady && messageQueue.length === 0) {
|
||||
coreEvents.emitFeedback(
|
||||
'info',
|
||||
!isConfigInitialized
|
||||
? 'Initializing... Prompts will be queued.'
|
||||
: 'Waiting for MCP servers to initialize... Slash commands are still available and prompts will be queued.',
|
||||
'Waiting for MCP servers to initialize... Slash commands are still available and prompts will be queued.',
|
||||
);
|
||||
}
|
||||
addMessage(submittedValue);
|
||||
@@ -1347,7 +1361,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isMcpReady,
|
||||
streamingState,
|
||||
messageQueue.length,
|
||||
pendingHistoryItems,
|
||||
pendingSlashCommandHistoryItems,
|
||||
pendingGeminiHistoryItems,
|
||||
config,
|
||||
constrainHeight,
|
||||
setConstrainHeight,
|
||||
@@ -1355,7 +1370,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
refreshStatic,
|
||||
reset,
|
||||
handleHintSubmit,
|
||||
isConfigInitialized,
|
||||
triggerExpandHint,
|
||||
],
|
||||
);
|
||||
@@ -1386,55 +1400,31 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
* - Any future streaming states not explicitly allowed
|
||||
*/
|
||||
const isInputActive =
|
||||
isConfigInitialized &&
|
||||
!initError &&
|
||||
!isProcessing &&
|
||||
!isResuming &&
|
||||
!!slashCommands &&
|
||||
(streamingState === StreamingState.Idle ||
|
||||
streamingState === StreamingState.Responding ||
|
||||
streamingState === StreamingState.WaitingForConfirmation) &&
|
||||
!proQuotaRequest &&
|
||||
!copyModeEnabled;
|
||||
streamingState === StreamingState.Responding) &&
|
||||
!proQuotaRequest;
|
||||
|
||||
const observerRef = useRef<ResizeObserver | null>(null);
|
||||
const [controlsHeight, setControlsHeight] = useState(0);
|
||||
const [lastNonCopyControlsHeight, setLastNonCopyControlsHeight] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!copyModeEnabled && controlsHeight > 0) {
|
||||
setLastNonCopyControlsHeight(controlsHeight);
|
||||
if (mainControlsRef.current) {
|
||||
const fullFooterMeasurement = measureElement(mainControlsRef.current);
|
||||
const roundedHeight = Math.round(fullFooterMeasurement.height);
|
||||
if (roundedHeight > 0 && roundedHeight !== controlsHeight) {
|
||||
setControlsHeight(roundedHeight);
|
||||
}
|
||||
}
|
||||
}, [copyModeEnabled, controlsHeight]);
|
||||
}, [buffer, terminalWidth, terminalHeight, controlsHeight]);
|
||||
|
||||
const stableControlsHeight =
|
||||
copyModeEnabled && lastNonCopyControlsHeight > 0
|
||||
? lastNonCopyControlsHeight
|
||||
: controlsHeight;
|
||||
|
||||
const mainControlsRef = useCallback((node: DOMElement | null) => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
observerRef.current = null;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
const roundedHeight = Math.round(entry.contentRect.height);
|
||||
setControlsHeight((prev) =>
|
||||
roundedHeight !== prev ? roundedHeight : prev,
|
||||
);
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
observerRef.current = observer;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Compute available terminal height based on stable controls measurement
|
||||
// Compute available terminal height based on controls measurement
|
||||
const availableTerminalHeight = Math.max(
|
||||
0,
|
||||
terminalHeight - stableControlsHeight - backgroundShellHeight - 1,
|
||||
terminalHeight - controlsHeight - backgroundShellHeight - 1,
|
||||
);
|
||||
|
||||
config.setShellExecutionConfig({
|
||||
@@ -1683,13 +1673,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
[handleSlashCommand, settings],
|
||||
);
|
||||
|
||||
const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator({
|
||||
streamingState,
|
||||
shouldShowFocusHint,
|
||||
retryStatus,
|
||||
loadingPhrasesMode: settings.merged.ui.loadingPhrases,
|
||||
customWittyPhrases: settings.merged.ui.customWittyPhrases,
|
||||
errorVerbosity: settings.merged.ui.errorVerbosity,
|
||||
});
|
||||
|
||||
const handleGlobalKeypress = useCallback(
|
||||
(key: Key): boolean => {
|
||||
// Debug log keystrokes if enabled
|
||||
if (settings.merged.general.debugKeystrokeLogging) {
|
||||
debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
|
||||
}
|
||||
|
||||
if (shortcutsHelpVisible && isHelpDismissKey(key)) {
|
||||
setShortcutsHelpVisible(false);
|
||||
}
|
||||
@@ -1708,10 +1702,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
handleCtrlCPress();
|
||||
return true;
|
||||
} else if (keyMatchers[Command.EXIT](key)) {
|
||||
// If the input field is non-empty, do not exit.
|
||||
if (bufferRef.current.text.length > 0) {
|
||||
return false;
|
||||
}
|
||||
handleCtrlDPress();
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
|
||||
@@ -1872,7 +1862,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
activePtyId,
|
||||
handleSuspend,
|
||||
embeddedShellFocused,
|
||||
settings.merged.general.debugKeystrokeLogging,
|
||||
refreshStatic,
|
||||
setCopyModeEnabled,
|
||||
tabFocusTimeoutRef,
|
||||
@@ -2033,6 +2022,16 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
authState === AuthState.AwaitingApiKeyInput ||
|
||||
!!newAgents;
|
||||
|
||||
const pendingHistoryItems = useMemo(
|
||||
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
|
||||
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
|
||||
);
|
||||
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() => isToolAwaitingConfirmation(pendingHistoryItems),
|
||||
[pendingHistoryItems],
|
||||
);
|
||||
|
||||
const hasConfirmUpdateExtensionRequests =
|
||||
confirmUpdateExtensionRequests.length > 0;
|
||||
const hasLoopDetectionConfirmationRequest =
|
||||
@@ -2050,48 +2049,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
!!emptyWalletRequest ||
|
||||
!!customDialog;
|
||||
|
||||
const loadingPhrases = settings.merged.ui.loadingPhrases;
|
||||
const showStatusTips = loadingPhrases === 'tips' || loadingPhrases === 'all';
|
||||
const showStatusWit = loadingPhrases === 'witty' || loadingPhrases === 'all';
|
||||
|
||||
const showLoadingIndicator =
|
||||
(!embeddedShellFocused || isBackgroundShellVisible) &&
|
||||
streamingState === StreamingState.Responding &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
let estimatedStatusLength = 0;
|
||||
if (activeHooks.length > 0 && settings.merged.hooksConfig.notifications) {
|
||||
const hookLabel =
|
||||
activeHooks.length > 1 ? 'Executing Hooks' : 'Executing Hook';
|
||||
const hookNames = activeHooks
|
||||
.map(
|
||||
(h) =>
|
||||
h.name +
|
||||
(h.index && h.total && h.total > 1 ? ` (${h.index}/${h.total})` : ''),
|
||||
)
|
||||
.join(', ');
|
||||
estimatedStatusLength = hookLabel.length + hookNames.length + 10;
|
||||
} else if (showLoadingIndicator) {
|
||||
const thoughtText = thought?.subject || 'Waiting for model...';
|
||||
estimatedStatusLength = thoughtText.length + 25;
|
||||
} else if (hasPendingActionRequired) {
|
||||
estimatedStatusLength = 35;
|
||||
}
|
||||
|
||||
const maxLength = terminalWidth - estimatedStatusLength - 5;
|
||||
|
||||
const { elapsedTime, currentLoadingPhrase, currentTip, currentWittyPhrase } =
|
||||
useLoadingIndicator({
|
||||
streamingState,
|
||||
shouldShowFocusHint,
|
||||
retryStatus,
|
||||
showTips: showStatusTips,
|
||||
showWit: showStatusWit,
|
||||
customWittyPhrases: settings.merged.ui.customWittyPhrases,
|
||||
errorVerbosity: settings.merged.ui.errorVerbosity,
|
||||
maxLength,
|
||||
});
|
||||
|
||||
const allowPlanMode =
|
||||
config.isPlanEnabled() &&
|
||||
streamingState === StreamingState.Idle &&
|
||||
@@ -2164,7 +2121,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
]);
|
||||
|
||||
const allToolCalls = useMemo(
|
||||
() => getAllToolCalls(pendingHistoryItems),
|
||||
() =>
|
||||
pendingHistoryItems
|
||||
.filter(
|
||||
(item): item is HistoryItemToolGroup => item.type === 'tool_group',
|
||||
)
|
||||
.flatMap((item) => item.tools),
|
||||
[pendingHistoryItems],
|
||||
);
|
||||
|
||||
@@ -2272,8 +2234,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isFocused,
|
||||
elapsedTime,
|
||||
currentLoadingPhrase,
|
||||
currentTip,
|
||||
currentWittyPhrase,
|
||||
historyRemountKey,
|
||||
activeHooks,
|
||||
messageQueue,
|
||||
@@ -2293,7 +2253,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
availableTerminalHeight,
|
||||
stableControlsHeight,
|
||||
mainAreaWidth,
|
||||
staticAreaMaxItemHeight,
|
||||
staticExtraHeight,
|
||||
@@ -2332,7 +2291,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
newAgents,
|
||||
showIsExpandableHint,
|
||||
hintMode:
|
||||
config.isModelSteeringEnabled() && isToolExecuting(pendingHistoryItems),
|
||||
config.isModelSteeringEnabled() &&
|
||||
isToolExecuting([
|
||||
...pendingSlashCommandHistoryItems,
|
||||
...pendingGeminiHistoryItems,
|
||||
]),
|
||||
hintBuffer: '',
|
||||
}),
|
||||
[
|
||||
@@ -2398,8 +2361,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isFocused,
|
||||
elapsedTime,
|
||||
currentLoadingPhrase,
|
||||
currentTip,
|
||||
currentWittyPhrase,
|
||||
historyRemountKey,
|
||||
activeHooks,
|
||||
messageQueue,
|
||||
@@ -2415,7 +2376,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
availableTerminalHeight,
|
||||
stableControlsHeight,
|
||||
mainAreaWidth,
|
||||
staticAreaMaxItemHeight,
|
||||
staticExtraHeight,
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { cleanup, renderWithProviders } from '../test-utils/render.js';
|
||||
import { createMockSettings } from '../test-utils/settings.js';
|
||||
import { App } from './App.js';
|
||||
import {
|
||||
CoreToolCallStatus,
|
||||
ApprovalMode,
|
||||
makeFakeConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type UIState } from './contexts/UIStateContext.js';
|
||||
import type { SerializableConfirmationDetails } from '@google/gemini-cli-core';
|
||||
import { act } from 'react';
|
||||
import { StreamingState } from './types.js';
|
||||
|
||||
vi.mock('ink', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('ink')>();
|
||||
return {
|
||||
...original,
|
||||
useIsScreenReaderEnabled: vi.fn(() => false),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('./components/GeminiSpinner.js', () => ({
|
||||
GeminiSpinner: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('./components/CliSpinner.js', () => ({
|
||||
CliSpinner: () => null,
|
||||
}));
|
||||
|
||||
// Mock hooks to align with codebase style, even if App uses UIState directly
|
||||
vi.mock('./hooks/useGeminiStream.js');
|
||||
vi.mock('./hooks/useHistoryManager.js');
|
||||
vi.mock('./hooks/useQuotaAndFallback.js');
|
||||
vi.mock('./hooks/useThemeCommand.js');
|
||||
vi.mock('./auth/useAuth.js');
|
||||
vi.mock('./hooks/useEditorSettings.js');
|
||||
vi.mock('./hooks/useSettingsCommand.js');
|
||||
vi.mock('./hooks/useModelCommand.js');
|
||||
vi.mock('./hooks/slashCommandProcessor.js');
|
||||
vi.mock('./hooks/useConsoleMessages.js');
|
||||
vi.mock('./hooks/useTerminalSize.js', () => ({
|
||||
useTerminalSize: vi.fn(() => ({ columns: 100, rows: 30 })),
|
||||
}));
|
||||
|
||||
describe('Full Terminal Tool Confirmation Snapshot', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders tool confirmation box in the frame of the entire terminal', async () => {
|
||||
// Generate a large diff to warrant truncation
|
||||
let largeDiff =
|
||||
'--- a/packages/cli/src/ui/components/InputPrompt.tsx\n+++ b/packages/cli/src/ui/components/InputPrompt.tsx\n@@ -1,100 +1,105 @@\n';
|
||||
for (let i = 1; i <= 60; i++) {
|
||||
largeDiff += ` const line${i} = true;\n`;
|
||||
}
|
||||
largeDiff += '- return kittyProtocolSupporte...;\n';
|
||||
largeDiff += '+ return kittyProtocolSupporte...;\n';
|
||||
largeDiff += ' buffer: TextBuffer;\n';
|
||||
largeDiff += ' onSubmit: (value: string) => void;';
|
||||
|
||||
const confirmationDetails: SerializableConfirmationDetails = {
|
||||
type: 'edit',
|
||||
title: 'Edit packages/.../InputPrompt.tsx',
|
||||
fileName: 'InputPrompt.tsx',
|
||||
filePath: 'packages/.../InputPrompt.tsx',
|
||||
fileDiff: largeDiff,
|
||||
originalContent: 'old',
|
||||
newContent: 'new',
|
||||
isModifying: false,
|
||||
};
|
||||
|
||||
const toolCalls = [
|
||||
{
|
||||
callId: 'call-1-modify-selected',
|
||||
name: 'Edit',
|
||||
description:
|
||||
'packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProtocolSupporte...',
|
||||
status: CoreToolCallStatus.AwaitingApproval,
|
||||
resultDisplay: '',
|
||||
confirmationDetails,
|
||||
},
|
||||
];
|
||||
|
||||
const mockUIState = {
|
||||
history: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
text: 'Can you edit InputPrompt.tsx for me?',
|
||||
},
|
||||
],
|
||||
mainAreaWidth: 99,
|
||||
availableTerminalHeight: 36,
|
||||
streamingState: StreamingState.WaitingForConfirmation,
|
||||
constrainHeight: true,
|
||||
isConfigInitialized: true,
|
||||
cleanUiDetailsVisible: true,
|
||||
quota: {
|
||||
userTier: 'PRO',
|
||||
stats: {
|
||||
limits: {},
|
||||
usage: {},
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
},
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
id: 2,
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
showApprovalModeIndicator: ApprovalMode.DEFAULT,
|
||||
sessionStats: {
|
||||
lastPromptTokenCount: 175400,
|
||||
contextPercentage: 3,
|
||||
},
|
||||
buffer: { text: '' },
|
||||
messageQueue: [],
|
||||
activeHooks: [],
|
||||
contextFileNames: [],
|
||||
rootUiRef: { current: null },
|
||||
} as unknown as UIState;
|
||||
|
||||
const mockConfig = makeFakeConfig();
|
||||
mockConfig.getUseAlternateBuffer = () => true;
|
||||
mockConfig.isTrustedFolder = () => true;
|
||||
mockConfig.getDisableAlwaysAllow = () => false;
|
||||
mockConfig.getIdeMode = () => false;
|
||||
mockConfig.getTargetDir = () => '/directory';
|
||||
|
||||
const { waitUntilReady, lastFrame, generateSvg, unmount } =
|
||||
await renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
config: mockConfig,
|
||||
settings: createMockSettings({
|
||||
merged: {
|
||||
ui: {
|
||||
useAlternateBuffer: true,
|
||||
theme: 'default',
|
||||
showUserIdentity: false,
|
||||
showShortcutsHint: false,
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
hideTokens: false,
|
||||
hideModel: false,
|
||||
},
|
||||
},
|
||||
security: {
|
||||
enablePermanentToolApproval: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
// Give it a moment to render
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
});
|
||||
|
||||
await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -37,8 +37,8 @@ Tips for getting started:
|
||||
|
||||
|
||||
|
||||
Notifications
|
||||
|
||||
Notifications
|
||||
Composer
|
||||
"
|
||||
`;
|
||||
@@ -102,8 +102,8 @@ exports[`App > Snapshots > renders with dialogs visible 1`] = `
|
||||
|
||||
|
||||
|
||||
Notifications
|
||||
|
||||
Notifications
|
||||
DialogManager
|
||||
"
|
||||
`;
|
||||
@@ -145,8 +145,8 @@ HistoryItemDisplay
|
||||
|
||||
|
||||
|
||||
Notifications
|
||||
|
||||
Notifications
|
||||
Composer
|
||||
"
|
||||
`;
|
||||
|
||||
-266
@@ -1,266 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="666" viewBox="0 0 920 666">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="666" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<rect x="0" y="0" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="2" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀</text>
|
||||
<rect x="0" y="17" width="9" height="17" fill="#141414" />
|
||||
<rect x="9" y="17" width="18" height="17" fill="#141414" />
|
||||
<text x="9" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<rect x="27" y="17" width="324" height="17" fill="#141414" />
|
||||
<text x="27" y="19" fill="#ffffff" textLength="324" lengthAdjust="spacingAndGlyphs">Can you edit InputPrompt.tsx for me?</text>
|
||||
<rect x="351" y="17" width="549" height="17" fill="#141414" />
|
||||
<rect x="0" y="34" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="36" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄</text>
|
||||
<text x="0" y="53" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╭─────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="0" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffaf" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold">Action Required</text>
|
||||
<text x="882" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">?</text>
|
||||
<text x="45" y="104" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs" font-weight="bold">Edit</text>
|
||||
<text x="90" y="104" fill="#afafaf" textLength="774" lengthAdjust="spacingAndGlyphs">packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto</text>
|
||||
<text x="864" y="104" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">… </text>
|
||||
<text x="882" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="138" fill="#afafaf" textLength="414" lengthAdjust="spacingAndGlyphs">... first 44 lines hidden (Ctrl+O to show) ...</text>
|
||||
<text x="882" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="155" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">45</text>
|
||||
<text x="63" y="155" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="155" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line45</text>
|
||||
<text x="171" y="155" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="155" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="155" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="172" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">46</text>
|
||||
<text x="63" y="172" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="172" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line46</text>
|
||||
<text x="171" y="172" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="172" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="172" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="189" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">47</text>
|
||||
<text x="63" y="189" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="189" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line47</text>
|
||||
<text x="171" y="189" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="189" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="189" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="206" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">48</text>
|
||||
<text x="63" y="206" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="206" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line48</text>
|
||||
<text x="171" y="206" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="206" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="206" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="223" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">49</text>
|
||||
<text x="63" y="223" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="223" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line49</text>
|
||||
<text x="171" y="223" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="223" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="223" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="240" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">50</text>
|
||||
<text x="63" y="240" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="240" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line50</text>
|
||||
<text x="171" y="240" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="240" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="240" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="257" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">51</text>
|
||||
<text x="63" y="257" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="257" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line51</text>
|
||||
<text x="171" y="257" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="257" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="257" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="274" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">52</text>
|
||||
<text x="63" y="274" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="274" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line52</text>
|
||||
<text x="171" y="274" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="274" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="274" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="291" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">53</text>
|
||||
<text x="63" y="291" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="291" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line53</text>
|
||||
<text x="171" y="291" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="291" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="291" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="308" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">54</text>
|
||||
<text x="63" y="308" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="308" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line54</text>
|
||||
<text x="171" y="308" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="308" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="308" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="325" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">55</text>
|
||||
<text x="63" y="325" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="325" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line55</text>
|
||||
<text x="171" y="325" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="325" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="325" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="342" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">56</text>
|
||||
<text x="63" y="342" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="342" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line56</text>
|
||||
<text x="171" y="342" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="342" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="342" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="359" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">57</text>
|
||||
<text x="63" y="359" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="359" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line57</text>
|
||||
<text x="171" y="359" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="359" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="359" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="376" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">58</text>
|
||||
<text x="63" y="376" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="376" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line58</text>
|
||||
<text x="171" y="376" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="376" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="376" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="393" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">59</text>
|
||||
<text x="63" y="393" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="393" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line59</text>
|
||||
<text x="171" y="393" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="393" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="393" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="410" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">60</text>
|
||||
<text x="63" y="410" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="410" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line60</text>
|
||||
<text x="171" y="410" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="410" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="410" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="18" y="425" width="18" height="17" fill="#5f0000" />
|
||||
<text x="18" y="427" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</text>
|
||||
<rect x="36" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<rect x="45" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<text x="45" y="427" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
|
||||
<rect x="54" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<rect x="63" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<rect x="72" y="425" width="54" height="17" fill="#5f0000" />
|
||||
<text x="72" y="427" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="126" y="425" width="234" height="17" fill="#5f0000" />
|
||||
<text x="126" y="427" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
|
||||
<text x="882" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="18" y="442" width="18" height="17" fill="#005f00" />
|
||||
<text x="18" y="444" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</text>
|
||||
<rect x="36" y="442" width="9" height="17" fill="#005f00" />
|
||||
<rect x="45" y="442" width="9" height="17" fill="#005f00" />
|
||||
<text x="45" y="444" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
|
||||
<rect x="54" y="442" width="9" height="17" fill="#005f00" />
|
||||
<rect x="63" y="442" width="9" height="17" fill="#005f00" />
|
||||
<rect x="72" y="442" width="54" height="17" fill="#005f00" />
|
||||
<text x="72" y="444" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="126" y="442" width="234" height="17" fill="#005f00" />
|
||||
<text x="126" y="444" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
|
||||
<text x="882" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="461" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">62</text>
|
||||
<text x="63" y="461" fill="#e5e5e5" textLength="180" lengthAdjust="spacingAndGlyphs"> buffer: TextBuffer;</text>
|
||||
<text x="882" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="478" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">63</text>
|
||||
<text x="72" y="478" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">onSubmit</text>
|
||||
<text x="144" y="478" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs">: (</text>
|
||||
<text x="171" y="478" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">value</text>
|
||||
<text x="216" y="478" fill="#e5e5e5" textLength="18" lengthAdjust="spacingAndGlyphs">: </text>
|
||||
<text x="234" y="478" fill="#00cdcd" textLength="54" lengthAdjust="spacingAndGlyphs">string</text>
|
||||
<text x="288" y="478" fill="#e5e5e5" textLength="45" lengthAdjust="spacingAndGlyphs">) => </text>
|
||||
<text x="333" y="478" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">void</text>
|
||||
<text x="369" y="478" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="495" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Apply this change?</text>
|
||||
<text x="882" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="18" y="527" width="9" height="17" fill="#001a00" />
|
||||
<text x="18" y="529" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="27" y="527" width="9" height="17" fill="#001a00" />
|
||||
<rect x="36" y="527" width="18" height="17" fill="#001a00" />
|
||||
<text x="36" y="529" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
|
||||
<rect x="54" y="527" width="9" height="17" fill="#001a00" />
|
||||
<rect x="63" y="527" width="90" height="17" fill="#001a00" />
|
||||
<text x="63" y="529" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
|
||||
<rect x="153" y="527" width="288" height="17" fill="#001a00" />
|
||||
<text x="882" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="546" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
|
||||
<text x="63" y="546" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
|
||||
<text x="882" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="563" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
|
||||
<text x="63" y="563" fill="#ffffff" textLength="378" lengthAdjust="spacingAndGlyphs">Allow for this file in all future sessions</text>
|
||||
<text x="882" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="580" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="580" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">4.</text>
|
||||
<text x="63" y="580" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Modify with external editor</text>
|
||||
<text x="882" y="580" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="597" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="597" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">5.</text>
|
||||
<text x="63" y="597" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
|
||||
<text x="882" y="597" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="614" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="614" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="631" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╰─────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,43 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Full Terminal Tool Confirmation Snapshot > renders tool confirmation box in the frame of the entire terminal 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Can you edit InputPrompt.tsx for me?
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Action Required │
|
||||
│ │
|
||||
│ ? Edit packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto… │
|
||||
│ │
|
||||
│ ... first 44 lines hidden (Ctrl+O to show) ... │
|
||||
│ 45 const line45 = true; │
|
||||
│ 46 const line46 = true; │
|
||||
│ 47 const line47 = true; │█
|
||||
│ 48 const line48 = true; │█
|
||||
│ 49 const line49 = true; │█
|
||||
│ 50 const line50 = true; │█
|
||||
│ 51 const line51 = true; │█
|
||||
│ 52 const line52 = true; │█
|
||||
│ 53 const line53 = true; │█
|
||||
│ 54 const line54 = true; │█
|
||||
│ 55 const line55 = true; │█
|
||||
│ 56 const line56 = true; │█
|
||||
│ 57 const line57 = true; │█
|
||||
│ 58 const line58 = true; │█
|
||||
│ 59 const line59 = true; │█
|
||||
│ 60 const line60 = true; │█
|
||||
│ 61 - return kittyProtocolSupporte...; │█
|
||||
│ 61 + return kittyProtocolSupporte...; │█
|
||||
│ 62 buffer: TextBuffer; │█
|
||||
│ 63 onSubmit: (value: string) => void; │█
|
||||
│ Apply this change? │█
|
||||
│ │█
|
||||
│ ● 1. Allow once │█
|
||||
│ 2. Allow for this session │█
|
||||
│ 3. Allow for this file in all future sessions │█
|
||||
│ 4. Modify with external editor │█
|
||||
│ 5. No, suggest changes (esc) │█
|
||||
│ │█
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯█
|
||||
"
|
||||
`;
|
||||
@@ -254,7 +254,7 @@ describe('AuthDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('always shows API key dialog even when env var is present', async () => {
|
||||
it('skips API key dialog on initial setup if env var is present', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
|
||||
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
|
||||
@@ -265,12 +265,12 @@ describe('AuthDialog', () => {
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
|
||||
expect(props.setAuthState).toHaveBeenCalledWith(
|
||||
AuthState.AwaitingApiKeyInput,
|
||||
AuthState.Unauthenticated,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('always shows API key dialog even when env var is empty string', async () => {
|
||||
it('skips API key dialog if env var is present but empty', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
vi.stubEnv('GEMINI_API_KEY', ''); // Empty string
|
||||
// props.settings.merged.security.auth.selectedType is undefined here
|
||||
@@ -281,7 +281,7 @@ describe('AuthDialog', () => {
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
|
||||
expect(props.setAuthState).toHaveBeenCalledWith(
|
||||
AuthState.AwaitingApiKeyInput,
|
||||
AuthState.Unauthenticated,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
@@ -302,10 +302,10 @@ describe('AuthDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('always shows API key dialog on re-auth even if env var is present', async () => {
|
||||
it('skips API key dialog on re-auth if env var is present (cannot edit)', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
|
||||
// Simulate switching from a different auth method (e.g., Google Login → API key)
|
||||
// Simulate that the user has already authenticated once
|
||||
props.settings.merged.security.auth.selectedType =
|
||||
AuthType.LOGIN_WITH_GOOGLE;
|
||||
|
||||
@@ -315,7 +315,7 @@ describe('AuthDialog', () => {
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
|
||||
expect(props.setAuthState).toHaveBeenCalledWith(
|
||||
AuthState.AwaitingApiKeyInput,
|
||||
AuthState.Unauthenticated,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -137,11 +137,13 @@ export function AuthDialog({
|
||||
}
|
||||
|
||||
if (authType === AuthType.USE_GEMINI) {
|
||||
// Always show the API key input dialog so the user can
|
||||
// explicitly enter or confirm their key, regardless of
|
||||
// whether GEMINI_API_KEY env var or a stored key exists.
|
||||
setAuthState(AuthState.AwaitingApiKeyInput);
|
||||
return;
|
||||
if (process.env['GEMINI_API_KEY'] !== undefined) {
|
||||
setAuthState(AuthState.Unauthenticated);
|
||||
return;
|
||||
} else {
|
||||
setAuthState(AuthState.AwaitingApiKeyInput);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
setAuthState(AuthState.Unauthenticated);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
renderWithProviders,
|
||||
persistentStateMock,
|
||||
} from '../../test-utils/render.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { makeFakeConfig } from '@google/gemini-cli-core';
|
||||
@@ -265,23 +264,4 @@ describe('<AppHeader />', () => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT render Tips when ui.hideTips is true', async () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
config: mockConfig,
|
||||
settings: {
|
||||
merged: {
|
||||
ui: { hideTips: true },
|
||||
},
|
||||
} as unknown as LoadedSettings,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('Tips');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
|
||||
Gemini CLI
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}> v{version}</Text>
|
||||
{updateInfo?.isUpdating && (
|
||||
{updateInfo && (
|
||||
<Box marginLeft={2}>
|
||||
<Text color={theme.text.secondary}>
|
||||
<CliSpinner /> Updating
|
||||
|
||||
@@ -287,7 +287,7 @@ describe('AskUserDialog', () => {
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ useAlternateBuffer: true, expectedArrows: true },
|
||||
{ useAlternateBuffer: true, expectedArrows: false },
|
||||
{ useAlternateBuffer: false, expectedArrows: true },
|
||||
])(
|
||||
'Scroll Arrows (useAlternateBuffer: $useAlternateBuffer)',
|
||||
@@ -1453,85 +1453,4 @@ describe('AskUserDialog', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows at least 3 selection options even in small terminal heights', async () => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
question:
|
||||
'A very long question that would normally take up most of the space and squeeze the list if we did not have a heuristic to prevent it. This line is just to make it longer. And another one. Imagine this is a plan.',
|
||||
header: 'Test',
|
||||
type: QuestionType.CHOICE,
|
||||
options: [
|
||||
{ label: 'Option 1', description: 'Description 1' },
|
||||
{ label: 'Option 2', description: 'Description 2' },
|
||||
{ label: 'Option 3', description: 'Description 3' },
|
||||
{ label: 'Option 4', description: 'Description 4' },
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={80}
|
||||
availableHeight={12} // Very small height
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
// Should show at least 3 options
|
||||
expect(frame).toContain('1. Option 1');
|
||||
expect(frame).toContain('2. Option 2');
|
||||
expect(frame).toContain('3. Option 3');
|
||||
});
|
||||
});
|
||||
|
||||
it('allows the question to exceed 15 lines in a tall terminal', async () => {
|
||||
const longQuestion = Array.from(
|
||||
{ length: 25 },
|
||||
(_, i) => `Line ${i + 1}`,
|
||||
).join('\n');
|
||||
const questions: Question[] = [
|
||||
{
|
||||
question: longQuestion,
|
||||
header: 'Tall Test',
|
||||
type: QuestionType.CHOICE,
|
||||
options: [
|
||||
{ label: 'Option 1', description: 'D1' },
|
||||
{ label: 'Option 2', description: 'D2' },
|
||||
{ label: 'Option 3', description: 'D3' },
|
||||
],
|
||||
multiSelect: false,
|
||||
unconstrainedHeight: false,
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={80}
|
||||
availableHeight={40} // Tall terminal
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
// Should show more than 15 lines of the question
|
||||
// (The limit was previously 15, so showing Line 20 proves it's working)
|
||||
expect(frame).toContain('Line 20');
|
||||
expect(frame).toContain('Line 25');
|
||||
// Should still show the options
|
||||
expect(frame).toContain('1. Option 1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -849,24 +849,16 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
? Math.max(1, availableHeight - overhead)
|
||||
: undefined;
|
||||
|
||||
// Reserve space for at least 3 items if more selectionItems available.
|
||||
const reservedListHeight = Math.min(selectionItems.length * 2, 6);
|
||||
const questionHeightLimit =
|
||||
listHeight && !isAlternateBuffer
|
||||
? question.unconstrainedHeight
|
||||
? Math.max(1, listHeight - selectionItems.length * 2)
|
||||
: Math.max(1, listHeight - Math.max(DIALOG_PADDING, reservedListHeight))
|
||||
: Math.min(15, Math.max(1, listHeight - DIALOG_PADDING))
|
||||
: undefined;
|
||||
|
||||
const maxItemsToShow =
|
||||
listHeight && (!isAlternateBuffer || availableHeight !== undefined)
|
||||
? Math.min(
|
||||
selectionItems.length,
|
||||
Math.max(
|
||||
1,
|
||||
Math.floor((listHeight - (questionHeightLimit ?? 0)) / 2),
|
||||
),
|
||||
)
|
||||
listHeight && questionHeightLimit
|
||||
? Math.max(1, Math.floor((listHeight - questionHeightLimit) / 2))
|
||||
: selectionItems.length;
|
||||
|
||||
return (
|
||||
|
||||
@@ -17,6 +17,13 @@ import {
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
// Mock VimModeContext hook
|
||||
vi.mock('../contexts/VimModeContext.js', () => ({
|
||||
useVimMode: vi.fn(() => ({
|
||||
vimEnabled: false,
|
||||
vimMode: 'INSERT',
|
||||
})),
|
||||
}));
|
||||
import {
|
||||
ApprovalMode,
|
||||
tokenLimit,
|
||||
@@ -29,21 +36,6 @@ import type { LoadedSettings } from '../../config/settings.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import type { TextBuffer } from './shared/text-buffer.js';
|
||||
|
||||
// Mock VimModeContext hook
|
||||
vi.mock('../contexts/VimModeContext.js', () => ({
|
||||
useVimMode: vi.fn(() => ({
|
||||
vimEnabled: false,
|
||||
vimMode: 'INSERT',
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useTerminalSize.js', () => ({
|
||||
useTerminalSize: vi.fn(() => ({
|
||||
columns: 100,
|
||||
rows: 24,
|
||||
})),
|
||||
}));
|
||||
|
||||
const composerTestControls = vi.hoisted(() => ({
|
||||
suggestionsVisible: false,
|
||||
isAlternateBuffer: false,
|
||||
@@ -66,9 +58,18 @@ vi.mock('./LoadingIndicator.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('./StatusDisplay.js', () => ({
|
||||
StatusDisplay: ({ hideContextSummary }: { hideContextSummary: boolean }) => (
|
||||
<Text>StatusDisplay{hideContextSummary ? ' (hidden summary)' : ''}</Text>
|
||||
),
|
||||
StatusDisplay: () => <Text>StatusDisplay</Text>,
|
||||
}));
|
||||
|
||||
vi.mock('./ToastDisplay.js', () => ({
|
||||
ToastDisplay: () => <Text>ToastDisplay</Text>,
|
||||
shouldShowToast: (uiState: UIState) =>
|
||||
uiState.ctrlCPressedOnce ||
|
||||
Boolean(uiState.transientMessage) ||
|
||||
uiState.ctrlDPressedOnce ||
|
||||
(uiState.showEscapePrompt &&
|
||||
(uiState.buffer.text.length > 0 || uiState.history.length > 0)) ||
|
||||
Boolean(uiState.queueErrorMessage),
|
||||
}));
|
||||
|
||||
vi.mock('./ContextSummaryDisplay.js', () => ({
|
||||
@@ -80,15 +81,17 @@ vi.mock('./HookStatusDisplay.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('./ApprovalModeIndicator.js', () => ({
|
||||
ApprovalModeIndicator: ({ approvalMode }: { approvalMode: ApprovalMode }) => (
|
||||
<Text>ApprovalModeIndicator: {approvalMode}</Text>
|
||||
),
|
||||
ApprovalModeIndicator: () => <Text>ApprovalModeIndicator</Text>,
|
||||
}));
|
||||
|
||||
vi.mock('./ShellModeIndicator.js', () => ({
|
||||
ShellModeIndicator: () => <Text>ShellModeIndicator</Text>,
|
||||
}));
|
||||
|
||||
vi.mock('./ShortcutsHint.js', () => ({
|
||||
ShortcutsHint: () => <Text>ShortcutsHint</Text>,
|
||||
}));
|
||||
|
||||
vi.mock('./ShortcutsHelp.js', () => ({
|
||||
ShortcutsHelp: () => <Text>ShortcutsHelp</Text>,
|
||||
}));
|
||||
@@ -171,8 +174,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
||||
isFocused: true,
|
||||
thought: '',
|
||||
currentLoadingPhrase: '',
|
||||
currentTip: '',
|
||||
currentWittyPhrase: '',
|
||||
elapsedTime: 0,
|
||||
ctrlCPressedOnce: false,
|
||||
ctrlDPressedOnce: false,
|
||||
@@ -200,7 +201,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
||||
activeHooks: [],
|
||||
isBackgroundShellVisible: false,
|
||||
embeddedShellFocused: false,
|
||||
showIsExpandableHint: false,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: undefined,
|
||||
@@ -247,7 +247,7 @@ const createMockConfig = (overrides = {}): Config =>
|
||||
|
||||
const renderComposer = async (
|
||||
uiState: UIState,
|
||||
settings = createMockSettings({ ui: {} }),
|
||||
settings = createMockSettings(),
|
||||
config = createMockConfig(),
|
||||
uiActions = createMockUIActions(),
|
||||
) => {
|
||||
@@ -256,7 +256,7 @@ const renderComposer = async (
|
||||
<SettingsContext.Provider value={settings as unknown as LoadedSettings}>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<UIActionsContext.Provider value={uiActions}>
|
||||
<Composer isFocused={true} />
|
||||
<Composer />
|
||||
</UIActionsContext.Provider>
|
||||
</UIStateContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
@@ -383,12 +383,10 @@ describe('Composer', () => {
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
|
||||
const output = lastFrame();
|
||||
// In Refreshed UX, we don't force 'Thinking...' label in renderStatusNode
|
||||
// It uses the subject directly
|
||||
expect(output).toContain('LoadingIndicator: Thinking about code');
|
||||
expect(output).toContain('LoadingIndicator: Thinking...');
|
||||
});
|
||||
|
||||
it('shows shortcuts hint while loading', async () => {
|
||||
it('hides shortcuts hint while loading', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
elapsedTime: 1,
|
||||
@@ -399,8 +397,7 @@ describe('Composer', () => {
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator');
|
||||
expect(output).toContain('press tab twice for more');
|
||||
expect(output).not.toContain('? for shortcuts');
|
||||
expect(output).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('renders LoadingIndicator with thought when loadingPhrases is off', async () => {
|
||||
@@ -456,8 +453,9 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame({ allowEmpty: true });
|
||||
expect(output).toBe('');
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('LoadingIndicator');
|
||||
expect(output).not.toContain('esc to cancel');
|
||||
});
|
||||
|
||||
it('renders LoadingIndicator when embedded shell is focused but background shell is visible', async () => {
|
||||
@@ -560,10 +558,8 @@ describe('Composer', () => {
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Press Ctrl+C again to exit.');
|
||||
// In Refreshed UX, Row 1 shows toast, and Row 2 shows ApprovalModeIndicator/StatusDisplay
|
||||
// They are no longer mutually exclusive.
|
||||
expect(output).toContain('ApprovalModeIndicator');
|
||||
expect(output).toContain('ToastDisplay');
|
||||
expect(output).not.toContain('ApprovalModeIndicator');
|
||||
expect(output).toContain('StatusDisplay');
|
||||
});
|
||||
|
||||
@@ -578,8 +574,8 @@ describe('Composer', () => {
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Warning');
|
||||
expect(output).toContain('ApprovalModeIndicator');
|
||||
expect(output).toContain('ToastDisplay');
|
||||
expect(output).not.toContain('ApprovalModeIndicator');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -588,17 +584,15 @@ describe('Composer', () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
});
|
||||
const settings = createMockSettings({
|
||||
ui: { showShortcutsHint: false },
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('press tab twice for more');
|
||||
expect(output).not.toContain('? for shortcuts');
|
||||
expect(output).toContain('ShortcutsHint');
|
||||
expect(output).toContain('InputPrompt');
|
||||
expect(output).not.toContain('Footer');
|
||||
expect(output).not.toContain('ApprovalModeIndicator');
|
||||
expect(output).not.toContain('ContextSummaryDisplay');
|
||||
});
|
||||
|
||||
it('renders InputPrompt when input is active', async () => {
|
||||
@@ -671,15 +665,12 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ mode: ApprovalMode.YOLO, label: '● YOLO' },
|
||||
{ mode: ApprovalMode.PLAN, label: '● plan' },
|
||||
{
|
||||
mode: ApprovalMode.AUTO_EDIT,
|
||||
label: '● auto edit',
|
||||
},
|
||||
[ApprovalMode.YOLO, 'YOLO'],
|
||||
[ApprovalMode.PLAN, 'plan'],
|
||||
[ApprovalMode.AUTO_EDIT, 'auto edit'],
|
||||
])(
|
||||
'shows minimal mode badge "$mode" when clean UI details are hidden',
|
||||
async ({ mode, label }) => {
|
||||
'shows minimal mode badge "%s" when clean UI details are hidden',
|
||||
async (mode, label) => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
showApprovalModeIndicator: mode,
|
||||
@@ -702,8 +693,7 @@ describe('Composer', () => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator');
|
||||
expect(output).not.toContain('plan');
|
||||
expect(output).toContain('press tab twice for more');
|
||||
expect(output).not.toContain('? for shortcuts');
|
||||
expect(output).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides minimal mode badge while action-required state is active', async () => {
|
||||
@@ -718,7 +708,9 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('plan');
|
||||
expect(output).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('shows Esc rewind prompt in minimal mode without showing full UI', async () => {
|
||||
@@ -730,7 +722,7 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Press Esc again to rewind.');
|
||||
expect(output).toContain('ToastDisplay');
|
||||
expect(output).not.toContain('ContextSummaryDisplay');
|
||||
});
|
||||
|
||||
@@ -755,14 +747,7 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
});
|
||||
|
||||
// StatusDisplay (which contains ContextUsageDisplay) should bleed through in minimal mode
|
||||
expect(lastFrame()).toContain('StatusDisplay');
|
||||
expect(lastFrame()).toContain('70% used');
|
||||
expect(lastFrame()).toContain('%');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -827,20 +812,14 @@ describe('Composer', () => {
|
||||
|
||||
describe('Shortcuts Hint', () => {
|
||||
it('restores shortcuts hint after 200ms debounce when buffer is empty', async () => {
|
||||
const uiState = createMockUIState({
|
||||
buffer: { text: '' } as unknown as TextBuffer,
|
||||
cleanUiDetailsVisible: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
});
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toContain(
|
||||
'press tab twice for more',
|
||||
const { lastFrame } = await renderComposer(
|
||||
createMockUIState({
|
||||
buffer: { text: '' } as unknown as TextBuffer,
|
||||
cleanUiDetailsVisible: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint when text is typed in buffer', async () => {
|
||||
@@ -851,8 +830,7 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('press tab twice for more');
|
||||
expect(lastFrame()).not.toContain('? for shortcuts');
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint when showShortcutsHint setting is false', async () => {
|
||||
@@ -865,7 +843,7 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
|
||||
expect(lastFrame()).not.toContain('? for shortcuts');
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint when a action is required (e.g. dialog is open)', async () => {
|
||||
@@ -878,10 +856,9 @@ describe('Composer', () => {
|
||||
),
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = await renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('keeps shortcuts hint visible when no action is required', async () => {
|
||||
@@ -891,11 +868,7 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('press tab twice for more');
|
||||
expect(lastFrame()).toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('shows shortcuts hint when full UI details are visible', async () => {
|
||||
@@ -905,15 +878,10 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
});
|
||||
|
||||
// In Refreshed UX, shortcuts hint is in the top multipurpose status row
|
||||
expect(lastFrame()).toContain('? for shortcuts');
|
||||
expect(lastFrame()).toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('shows shortcuts hint while loading when full UI details are visible', async () => {
|
||||
it('hides shortcuts hint while loading when full UI details are visible', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
@@ -921,17 +889,10 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
});
|
||||
|
||||
// In experimental layout, status row is visible during loading
|
||||
expect(lastFrame()).toContain('LoadingIndicator');
|
||||
expect(lastFrame()).toContain('? for shortcuts');
|
||||
expect(lastFrame()).not.toContain('press tab twice for more');
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('shows shortcuts hint while loading in minimal mode', async () => {
|
||||
it('hides shortcuts hint while loading in minimal mode', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
streamingState: StreamingState.Responding,
|
||||
@@ -940,14 +901,7 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
});
|
||||
|
||||
// In experimental layout, status row is visible in clean mode while busy
|
||||
expect(lastFrame()).toContain('LoadingIndicator');
|
||||
expect(lastFrame()).toContain('press tab twice for more');
|
||||
expect(lastFrame()).not.toContain('? for shortcuts');
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('shows shortcuts help in minimal mode when toggled on', async () => {
|
||||
@@ -972,8 +926,7 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('press tab twice for more');
|
||||
expect(lastFrame()).not.toContain('? for shortcuts');
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
expect(lastFrame()).not.toContain('plan');
|
||||
});
|
||||
|
||||
@@ -1001,12 +954,7 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
});
|
||||
|
||||
// In Refreshed UX, shortcuts hint is in the top status row and doesn't collide with suggestions below
|
||||
expect(lastFrame()).toContain('press tab twice for more');
|
||||
expect(lastFrame()).toContain('ShortcutsHint');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1034,22 +982,24 @@ describe('Composer', () => {
|
||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides shortcuts help when action is required', async () => {
|
||||
const uiState = createMockUIState({
|
||||
shortcutsHelpVisible: true,
|
||||
customDialog: (
|
||||
<Box>
|
||||
<Text>Test Dialog</Text>
|
||||
<Text>Dialog content</Text>
|
||||
</Box>
|
||||
),
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshots', () => {
|
||||
it('matches snapshot in idle state', async () => {
|
||||
const uiState = createMockUIState();
|
||||
|
||||
@@ -4,53 +4,89 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, useIsScreenReaderEnabled } from 'ink';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
|
||||
import {
|
||||
ApprovalMode,
|
||||
checkExhaustive,
|
||||
CoreToolCallStatus,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { LoadingIndicator } from './LoadingIndicator.js';
|
||||
import { StatusDisplay } from './StatusDisplay.js';
|
||||
import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
|
||||
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
|
||||
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
||||
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
||||
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
|
||||
import { ShortcutsHint } from './ShortcutsHint.js';
|
||||
import { ShortcutsHelp } from './ShortcutsHelp.js';
|
||||
import { InputPrompt } from './InputPrompt.js';
|
||||
import { Footer } from './Footer.js';
|
||||
import { StatusRow } from './StatusRow.js';
|
||||
import { ShowMoreLines } from './ShowMoreLines.js';
|
||||
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { HorizontalLine } from './shared/HorizontalLine.js';
|
||||
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
||||
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { StreamingState, type HistoryItemToolGroup } from '../types.js';
|
||||
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
||||
import { TodoTray } from './messages/Todo.js';
|
||||
import { useComposerStatus } from '../hooks/useComposerStatus.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
import { isContextUsageHigh } from '../utils/contextUsage.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const config = useConfig();
|
||||
const settings = useSettings();
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
const uiState = useUIState();
|
||||
const uiActions = useUIActions();
|
||||
const settings = useSettings();
|
||||
const config = useConfig();
|
||||
const { vimEnabled, vimMode } = useVimMode();
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
const { columns: terminalWidth } = useTerminalSize();
|
||||
const inlineThinkingMode = getInlineThinkingMode(settings);
|
||||
const terminalWidth = uiState.terminalWidth;
|
||||
const isNarrow = isNarrowWidth(terminalWidth);
|
||||
const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5));
|
||||
const [suggestionsVisible, setSuggestionsVisible] = useState(false);
|
||||
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const { showApprovalModeIndicator } = uiState;
|
||||
const showUiDetails = uiState.cleanUiDetailsVisible;
|
||||
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
|
||||
const hideContextSummary =
|
||||
suggestionsVisible && suggestionsPosition === 'above';
|
||||
|
||||
const { hasPendingActionRequired, shouldCollapseDuringApproval } =
|
||||
useComposerStatus();
|
||||
const hasPendingToolConfirmation = useMemo(
|
||||
() =>
|
||||
(uiState.pendingHistoryItems ?? [])
|
||||
.filter(
|
||||
(item): item is HistoryItemToolGroup => item.type === 'tool_group',
|
||||
)
|
||||
.some((item) =>
|
||||
item.tools.some(
|
||||
(tool) => tool.status === CoreToolCallStatus.AwaitingApproval,
|
||||
),
|
||||
),
|
||||
[uiState.pendingHistoryItems],
|
||||
);
|
||||
|
||||
const hasPendingActionRequired =
|
||||
hasPendingToolConfirmation ||
|
||||
Boolean(uiState.commandConfirmationRequest) ||
|
||||
Boolean(uiState.authConsentRequest) ||
|
||||
(uiState.confirmUpdateExtensionRequests?.length ?? 0) > 0 ||
|
||||
Boolean(uiState.loopDetectionConfirmationRequest) ||
|
||||
Boolean(uiState.quota.proQuotaRequest) ||
|
||||
Boolean(uiState.quota.validationRequest) ||
|
||||
Boolean(uiState.customDialog);
|
||||
const isPassiveShortcutsHelpState =
|
||||
uiState.isInputActive &&
|
||||
uiState.streamingState === 'idle' &&
|
||||
uiState.streamingState === StreamingState.Idle &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
const { setShortcutsHelpVisible } = uiActions;
|
||||
@@ -67,19 +103,91 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
|
||||
const showShortcutsHelp =
|
||||
uiState.shortcutsHelpVisible &&
|
||||
uiState.streamingState === 'idle' &&
|
||||
uiState.streamingState === StreamingState.Idle &&
|
||||
!hasPendingActionRequired;
|
||||
|
||||
if (hasPendingActionRequired && shouldCollapseDuringApproval) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasToast = shouldShowToast(uiState);
|
||||
const showLoadingIndicator =
|
||||
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
|
||||
uiState.streamingState === StreamingState.Responding &&
|
||||
!hasPendingActionRequired;
|
||||
const hideUiDetailsForSuggestions =
|
||||
suggestionsVisible && suggestionsPosition === 'above';
|
||||
const showApprovalIndicator =
|
||||
!uiState.shellModeActive && !hideUiDetailsForSuggestions;
|
||||
const showRawMarkdownIndicator = !uiState.renderMarkdown;
|
||||
let modeBleedThrough: { text: string; color: string } | null = null;
|
||||
switch (showApprovalModeIndicator) {
|
||||
case ApprovalMode.YOLO:
|
||||
modeBleedThrough = { text: 'YOLO', color: theme.status.error };
|
||||
break;
|
||||
case ApprovalMode.PLAN:
|
||||
modeBleedThrough = { text: 'plan', color: theme.status.success };
|
||||
break;
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
modeBleedThrough = { text: 'auto edit', color: theme.status.warning };
|
||||
break;
|
||||
case ApprovalMode.DEFAULT:
|
||||
modeBleedThrough = null;
|
||||
break;
|
||||
default:
|
||||
checkExhaustive(showApprovalModeIndicator);
|
||||
modeBleedThrough = null;
|
||||
break;
|
||||
}
|
||||
|
||||
// Mini Mode VIP Flags (Pure Content Triggers)
|
||||
const showMinimalToast = hasToast;
|
||||
const hideMinimalModeHintWhileBusy =
|
||||
!showUiDetails && (showLoadingIndicator || hasPendingActionRequired);
|
||||
const minimalModeBleedThrough = hideMinimalModeHintWhileBusy
|
||||
? null
|
||||
: modeBleedThrough;
|
||||
const hasMinimalStatusBleedThrough = shouldShowToast(uiState);
|
||||
|
||||
const showMinimalContextBleedThrough =
|
||||
!settings.merged.ui.footer.hideContextPercentage &&
|
||||
isContextUsageHigh(
|
||||
uiState.sessionStats.lastPromptTokenCount,
|
||||
typeof uiState.currentModel === 'string'
|
||||
? uiState.currentModel
|
||||
: undefined,
|
||||
);
|
||||
const hideShortcutsHintForSuggestions = hideUiDetailsForSuggestions;
|
||||
const isModelIdle = uiState.streamingState === StreamingState.Idle;
|
||||
const isBufferEmpty = uiState.buffer.text.length === 0;
|
||||
const canShowShortcutsHint =
|
||||
isModelIdle && isBufferEmpty && !hasPendingActionRequired;
|
||||
const [showShortcutsHintDebounced, setShowShortcutsHintDebounced] =
|
||||
useState(canShowShortcutsHint);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canShowShortcutsHint) {
|
||||
setShowShortcutsHintDebounced(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setShowShortcutsHintDebounced(true);
|
||||
}, 200);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [canShowShortcutsHint]);
|
||||
|
||||
const shouldReserveSpaceForShortcutsHint =
|
||||
settings.merged.ui.showShortcutsHint && !hideShortcutsHintForSuggestions;
|
||||
const showShortcutsHint =
|
||||
shouldReserveSpaceForShortcutsHint && showShortcutsHintDebounced;
|
||||
const showMinimalModeBleedThrough =
|
||||
!hideUiDetailsForSuggestions && Boolean(minimalModeBleedThrough);
|
||||
const showMinimalInlineLoading = !showUiDetails && showLoadingIndicator;
|
||||
const showMinimalBleedThroughRow =
|
||||
!showUiDetails &&
|
||||
(showMinimalModeBleedThrough ||
|
||||
hasMinimalStatusBleedThrough ||
|
||||
showMinimalContextBleedThrough);
|
||||
const showMinimalMetaRow =
|
||||
!showUiDetails &&
|
||||
(showMinimalInlineLoading ||
|
||||
showMinimalBleedThroughRow ||
|
||||
shouldReserveSpaceForShortcutsHint);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -88,8 +196,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
flexGrow={0}
|
||||
flexShrink={0}
|
||||
>
|
||||
{uiState.isResuming && (
|
||||
<ConfigInitDisplay message="Resuming session..." />
|
||||
{(!uiState.slashCommands ||
|
||||
!uiState.isConfigInitialized ||
|
||||
uiState.isResuming) && (
|
||||
<ConfigInitDisplay
|
||||
message={uiState.isResuming ? 'Resuming session...' : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showUiDetails && (
|
||||
@@ -98,23 +210,212 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
|
||||
{showUiDetails && <TodoTray />}
|
||||
|
||||
{showShortcutsHelp && <ShortcutsHelp />}
|
||||
|
||||
{(showUiDetails || showMinimalToast) && (
|
||||
<Box minHeight={1} marginLeft={isNarrow ? 0 : 1}>
|
||||
<ToastDisplay />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box width="100%" flexDirection="column">
|
||||
<StatusRow
|
||||
showUiDetails={showUiDetails}
|
||||
isNarrow={isNarrow}
|
||||
terminalWidth={terminalWidth}
|
||||
hideContextSummary={hideContextSummary}
|
||||
hideUiDetailsForSuggestions={hideUiDetailsForSuggestions}
|
||||
hasPendingActionRequired={hasPendingActionRequired}
|
||||
/>
|
||||
<Box
|
||||
width="100%"
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
justifyContent={isNarrow ? 'flex-start' : 'space-between'}
|
||||
>
|
||||
<Box
|
||||
marginLeft={1}
|
||||
marginRight={isNarrow ? 0 : 1}
|
||||
flexDirection="row"
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
flexGrow={1}
|
||||
>
|
||||
{showUiDetails && showLoadingIndicator && (
|
||||
<LoadingIndicator
|
||||
inline
|
||||
thought={
|
||||
uiState.streamingState ===
|
||||
StreamingState.WaitingForConfirmation
|
||||
? undefined
|
||||
: uiState.thought
|
||||
}
|
||||
currentLoadingPhrase={
|
||||
settings.merged.ui.loadingPhrases === 'off'
|
||||
? undefined
|
||||
: uiState.currentLoadingPhrase
|
||||
}
|
||||
thoughtLabel={
|
||||
inlineThinkingMode === 'full' ? 'Thinking...' : undefined
|
||||
}
|
||||
elapsedTime={uiState.elapsedTime}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
marginTop={isNarrow ? 1 : 0}
|
||||
flexDirection="column"
|
||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||
minHeight={
|
||||
showUiDetails && shouldReserveSpaceForShortcutsHint ? 1 : 0
|
||||
}
|
||||
>
|
||||
{showUiDetails && showShortcutsHint && <ShortcutsHint />}
|
||||
</Box>
|
||||
</Box>
|
||||
{showMinimalMetaRow && (
|
||||
<Box
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
>
|
||||
<Box
|
||||
marginLeft={1}
|
||||
marginRight={isNarrow ? 0 : 1}
|
||||
flexDirection="row"
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
flexGrow={1}
|
||||
>
|
||||
{showMinimalInlineLoading && (
|
||||
<LoadingIndicator
|
||||
inline
|
||||
thought={
|
||||
uiState.streamingState ===
|
||||
StreamingState.WaitingForConfirmation
|
||||
? undefined
|
||||
: uiState.thought
|
||||
}
|
||||
currentLoadingPhrase={
|
||||
settings.merged.ui.loadingPhrases === 'off'
|
||||
? undefined
|
||||
: uiState.currentLoadingPhrase
|
||||
}
|
||||
thoughtLabel={
|
||||
inlineThinkingMode === 'full' ? 'Thinking...' : undefined
|
||||
}
|
||||
elapsedTime={uiState.elapsedTime}
|
||||
/>
|
||||
)}
|
||||
{showMinimalModeBleedThrough && minimalModeBleedThrough && (
|
||||
<Text color={minimalModeBleedThrough.color}>
|
||||
● {minimalModeBleedThrough.text}
|
||||
</Text>
|
||||
)}
|
||||
{hasMinimalStatusBleedThrough && (
|
||||
<Box
|
||||
marginLeft={
|
||||
showMinimalInlineLoading || showMinimalModeBleedThrough
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
>
|
||||
<ToastDisplay />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{(showMinimalContextBleedThrough ||
|
||||
shouldReserveSpaceForShortcutsHint) && (
|
||||
<Box
|
||||
marginTop={isNarrow && showMinimalBleedThroughRow ? 1 : 0}
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||
minHeight={1}
|
||||
>
|
||||
{showMinimalContextBleedThrough && (
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={uiState.sessionStats.lastPromptTokenCount}
|
||||
model={uiState.currentModel}
|
||||
terminalWidth={uiState.terminalWidth}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
marginLeft={
|
||||
showMinimalContextBleedThrough && !isNarrow ? 1 : 0
|
||||
}
|
||||
marginTop={showMinimalContextBleedThrough && isNarrow ? 1 : 0}
|
||||
>
|
||||
{showShortcutsHint && <ShortcutsHint />}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{showShortcutsHelp && <ShortcutsHelp />}
|
||||
{showUiDetails && <HorizontalLine />}
|
||||
{showUiDetails && (
|
||||
<Box
|
||||
justifyContent={
|
||||
settings.merged.ui.hideContextSummary
|
||||
? 'flex-start'
|
||||
: 'space-between'
|
||||
}
|
||||
width="100%"
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
>
|
||||
<Box
|
||||
marginLeft={1}
|
||||
marginRight={isNarrow ? 0 : 1}
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
flexGrow={1}
|
||||
>
|
||||
{hasToast ? (
|
||||
<ToastDisplay />
|
||||
) : (
|
||||
<Box
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
>
|
||||
{showApprovalIndicator && (
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={showApprovalModeIndicator}
|
||||
allowPlanMode={uiState.allowPlanMode}
|
||||
/>
|
||||
)}
|
||||
{!showLoadingIndicator && (
|
||||
<>
|
||||
{uiState.shellModeActive && (
|
||||
<Box
|
||||
marginLeft={
|
||||
showApprovalIndicator && !isNarrow ? 1 : 0
|
||||
}
|
||||
marginTop={showApprovalIndicator && isNarrow ? 1 : 0}
|
||||
>
|
||||
<ShellModeIndicator />
|
||||
</Box>
|
||||
)}
|
||||
{showRawMarkdownIndicator && (
|
||||
<Box
|
||||
marginLeft={
|
||||
(showApprovalIndicator ||
|
||||
uiState.shellModeActive) &&
|
||||
!isNarrow
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
marginTop={
|
||||
(showApprovalIndicator ||
|
||||
uiState.shellModeActive) &&
|
||||
!isNarrow
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
>
|
||||
<RawMarkdownIndicator />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
marginTop={isNarrow ? 1 : 0}
|
||||
flexDirection="column"
|
||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||
>
|
||||
{!showLoadingIndicator && (
|
||||
<StatusDisplay hideContextSummary={hideContextSummary} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{showUiDetails && uiState.showErrorDetails && (
|
||||
@@ -146,7 +447,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
commandContext={uiState.commandContext}
|
||||
shellModeActive={uiState.shellModeActive}
|
||||
setShellModeActive={uiActions.setShellModeActive}
|
||||
approvalMode={uiState.showApprovalModeIndicator}
|
||||
approvalMode={showApprovalModeIndicator}
|
||||
onEscapePromptChange={uiActions.onEscapePromptChange}
|
||||
focus={isFocused}
|
||||
vimHandleInput={uiActions.vimHandleInput}
|
||||
@@ -165,15 +466,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
streamingState={uiState.streamingState}
|
||||
suggestionsPosition={suggestionsPosition}
|
||||
onSuggestionsVisibilityChange={setSuggestionsVisible}
|
||||
copyModeEnabled={uiState.copyModeEnabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showUiDetails &&
|
||||
!settings.merged.ui.hideFooter &&
|
||||
!isScreenReaderEnabled && (
|
||||
<Footer copyModeEnabled={uiState.copyModeEnabled} />
|
||||
)}
|
||||
!isScreenReaderEnabled && <Footer />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ import { GeminiSpinner } from './GeminiSpinner.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
export const ConfigInitDisplay = ({
|
||||
message: initialMessage = 'Working...',
|
||||
message: initialMessage = 'Initializing...',
|
||||
}: {
|
||||
message?: string;
|
||||
}) => {
|
||||
@@ -45,14 +45,14 @@ export const ConfigInitDisplay = ({
|
||||
const suffix = remaining > 0 ? `, +${remaining} more` : '';
|
||||
const mcpMessage = `Connecting to MCP servers... (${connected}/${clients.size}) - Waiting for: ${displayedServers}${suffix}`;
|
||||
setMessage(
|
||||
initialMessage && initialMessage !== 'Working...'
|
||||
initialMessage && initialMessage !== 'Initializing...'
|
||||
? `${initialMessage} (${mcpMessage})`
|
||||
: mcpMessage,
|
||||
);
|
||||
} else {
|
||||
const mcpMessage = `Connecting to MCP servers... (${connected}/${clients.size})`;
|
||||
setMessage(
|
||||
initialMessage && initialMessage !== 'Working...'
|
||||
initialMessage && initialMessage !== 'Initializing...'
|
||||
? `${initialMessage} (${mcpMessage})`
|
||||
: mcpMessage,
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user