Compare commits

..

17 Commits

Author SHA1 Message Date
g-samroberts e0500ec891 Update website issue template to pre-fill title. 2026-03-18 17:57:27 -07:00
g-samroberts 7cfb85f951 Final linter run 2026-03-18 16:37:14 -07:00
Sam Roberts c52ea61af1 Update configuration.md 2026-03-18 16:31:50 -07:00
Sam Roberts 8adc855c48 Update commands.md 2026-03-18 16:29:21 -07:00
Sam Roberts ea5eef7b90 Update index.md 2026-03-18 16:26:43 -07:00
Sam Roberts d0157c4869 Update authentication.md 2026-03-18 16:23:15 -07:00
Sam Roberts c0f1c9cc5b Update plan-mode.md 2026-03-18 16:18:07 -07:00
Sam Roberts 4df6d27ac2 Update troubleshooting.md 2026-03-18 16:16:06 -07:00
Sam Roberts e3a6b6ab60 Update telemetry.md 2026-03-18 16:08:52 -07:00
Sam Roberts a2ad637398 Update notifications.md 2026-03-18 15:31:25 -07:00
Sam Roberts a5b8ea4418 Update model-steering.md 2026-03-18 15:30:25 -07:00
Sam Roberts ac6b1b888d Update plan-mode-steering.md 2026-03-18 15:28:06 -07:00
Sam Roberts d6eeb2f99e Update SKILL.md 2026-03-18 15:26:28 -07:00
Sam Roberts c08903ba83 Merge branch 'main' into doc-skill-callouts 2026-03-18 15:22:55 -07:00
Sam Roberts bfa762bcbb Update configuration.md
Update configuration.md to remove subagents YOLO mode warning based on https://github.com/google-gemini/gemini-cli/pull/22386
2026-03-16 17:59:04 -07:00
Sam Roberts 0e5978ee47 Merge branch 'main' into doc-skill-callouts 2026-03-16 17:57:07 -07:00
g-samroberts 3502249e0c Update callouts to work on github. 2026-03-12 17:10:08 -07:00
262 changed files with 2331 additions and 6251 deletions
+1 -2
View File
@@ -2,8 +2,7 @@
"experimental": {
"plan": true,
"extensionReloading": true,
"modelSteering": true,
"memoryManager": true
"modelSteering": true
},
"general": {
"devtools": true
+1 -1
View File
@@ -1,6 +1,6 @@
name: 'Website issue'
description: 'Report an issue with the Gemini CLI Website and Gemini CLI Extensions Gallery'
title: 'GeminiCLI.com Feedback: [ISSUE]'
title: "GeminiCLI.com Feedback: [ISSUE]"
labels:
- 'area/extensions'
- 'area/documentation'
+3 -7
View File
@@ -1,6 +1,6 @@
# Preview release: v0.35.0-preview.2
# Preview release: v0.35.0-preview.1
Released: March 19, 2026
Released: March 17, 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).
@@ -33,10 +33,6 @@ npm install -g @google/gemini-cli@preview
## What's Changed
- 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)
- 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
@@ -377,4 +373,4 @@ npm install -g @google/gemini-cli@preview
[#22815](https://github.com/google-gemini/gemini-cli/pull/22815)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.34.0-preview.4...v0.35.0-preview.2
https://github.com/google-gemini/gemini-cli/compare/v0.34.0-preview.4...v0.35.0-preview.1
-1
View File
@@ -154,7 +154,6 @@ they appear in the UI.
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
### Skills
+5 -5
View File
@@ -7,9 +7,9 @@ create files, and control what Gemini CLI can see.
## Prerequisites
- Gemini CLI installed and authenticated.
- A project directory to work with (for example, a git repository).
- A project directory to work with (e.g., a git repository).
## Providing context by reading files
## How to give the agent context (Reading files)
Gemini CLI will generally try to read relevant files, sometimes prompting you
for access (depending on your settings). To ensure that Gemini CLI uses a file,
@@ -58,7 +58,7 @@ You know there's a `UserProfile` component, but you don't know where it lives.
```
Gemini uses the `glob` or `list_directory` tools to search your project
structure. It will return the specific path (for example,
structure. It will return the specific path (e.g.,
`src/components/UserProfile.tsx`), which you can then use with `@` in your next
turn.
@@ -113,8 +113,8 @@ or, better yet, run your project's tests.
`Run the tests for the UserProfile component.`
```
Gemini CLI uses the `run_shell_command` tool to execute your test runner (for
example, `npm test` or `jest`). This ensures the changes didn't break existing
Gemini CLI uses the `run_shell_command` tool to execute your test runner (e.g.,
`npm test` or `jest`). This ensures the changes didn't break existing
functionality.
## Advanced: Controlling what Gemini sees
+6 -6
View File
@@ -11,8 +11,8 @@ persistent facts, and inspect the active context.
## Why manage context?
Gemini CLI is powerful but general. It doesn't know your preferred testing
framework, your indentation style, or your preference against `any` in
Out of the box, Gemini CLI is smart but generic. It doesn't know your preferred
testing framework, your indentation style, or that you hate using `any` in
TypeScript. Context management solves this by giving the agent persistent
memory.
@@ -109,11 +109,11 @@ immediately. Force a reload with:
## Best practices
- **Keep it focused:** Avoid adding excessive content to `GEMINI.md`. Keep
instructions actionable and relevant to code generation.
- **Keep it focused:** Don't dump your entire internal wiki into `GEMINI.md`.
Keep instructions actionable and relevant to code generation.
- **Use negative constraints:** Explicitly telling the agent what _not_ to do
(for example, "Do not use class components") is often more effective than
vague positive instructions.
(e.g., "Do not use class components") is often more effective than vague
positive instructions.
- **Review often:** Periodically check your `GEMINI.md` files to remove outdated
rules.
+3 -3
View File
@@ -7,7 +7,7 @@ automate complex workflows, and manage background processes safely.
## Prerequisites
- Gemini CLI installed and authenticated.
- Basic familiarity with your system's shell (Bash, Zsh, PowerShell, and so on).
- Basic familiarity with your system's shell (Bash, Zsh, PowerShell, etc.).
## How to run commands directly (`!`)
@@ -49,7 +49,7 @@ You want to run tests and fix any failures.
6. Gemini uses `replace` to fix the bug.
7. Gemini runs `npm test` again to verify the fix.
This loop lets Gemini work autonomously.
This loop turns Gemini into an autonomous engineer.
## How to manage background processes
@@ -75,7 +75,7 @@ confirmation prompts) by streaming the output to you. However, for highly
interactive tools (like `vim` or `top`), it's often better to run them yourself
in a separate terminal window or use the `!` prefix.
## Safety features
## Safety first
Giving an AI access to your shell is powerful but risky. Gemini CLI includes
several safety layers.
+8 -8
View File
@@ -42,8 +42,8 @@ Select the authentication method that matches your situation in the table below:
If you run Gemini CLI on your local machine, the simplest authentication method
is logging in with your Google account. This method requires a web browser on a
machine that can communicate with the terminal running Gemini CLI (for example,
your local machine).
machine that can communicate with the terminal running Gemini CLI (e.g., your
local machine).
If you are a **Google AI Pro** or **Google AI Ultra** subscriber, use the Google
account associated with your subscription.
@@ -134,7 +134,7 @@ For example:
**macOS/Linux**
```bash
# Replace with your project ID and desired location (for example, us-central1)
# Replace with your project ID and desired location (e.g., us-central1)
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
```
@@ -142,7 +142,7 @@ export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
**Windows (PowerShell)**
```powershell
# Replace with your project ID and desired location (for example, us-central1)
# Replace with your project ID and desired location (e.g., us-central1)
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
$env:GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
```
@@ -333,14 +333,14 @@ persist them with the following methods:
1. **Add your environment variables to your shell configuration file:** Append
the environment variable commands to your shell's startup file.
**macOS/Linux** (for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
**macOS/Linux** (e.g., `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
```bash
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
source ~/.bashrc
```
**Windows (PowerShell)** (for example, `$PROFILE`):
**Windows (PowerShell)** (e.g., `$PROFILE`):
```powershell
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
@@ -356,8 +356,8 @@ persist them with the following methods:
2. **Use a `.env` file:** Create a `.gemini/.env` file in your project
directory or home directory. Gemini CLI automatically loads variables from
the first `.env` file it finds, searching up from the current directory,
then in your home directory's `.gemini/.env` (for example, `~/.gemini/.env`
or `%USERPROFILE%\.gemini\.env`).
then in your home directory's `.gemini/.env` (e.g., `~/.gemini/.env` or
`%USERPROFILE%\.gemini\.env`).
Example for user-wide settings:
+2 -2
View File
@@ -27,7 +27,7 @@ Get started by upgrading Gemini CLI to the latest version:
npm install -g @google/gemini-cli@latest
```
If your version is 0.21.1 or later:
After youve confirmed your version is 0.21.1 or later:
1. Run `/model`.
2. Select **Auto (Gemini 3)**.
@@ -115,7 +115,7 @@ then:
Restart Gemini CLI and you should have access to Gemini 3.
## Next steps
## Need help?
If you need help, we recommend searching for an existing
[GitHub issue](https://github.com/google-gemini/gemini-cli/issues). If you
-7
View File
@@ -1433,13 +1433,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `"gemma3-1b-gpu-custom"`
- **Requires restart:** Yes
- **`experimental.memoryManager`** (boolean):
- **Description:** Replace the built-in save_memory tool with a memory manager
subagent that supports adding, removing, de-duplicating, and organizing
memories.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.topicUpdateNarration`** (boolean):
- **Description:** Enable the experimental Topic & Update communication model
for reduced chattiness and structured progress reporting.
-13
View File
@@ -58,19 +58,6 @@ your total token usage using the `/stats` command in Gemini CLI.
## Installation and updates
### How do I check which version of Gemini CLI I'm currently running?
You can check your current Gemini CLI version using one of these methods:
- Run `gemini --version` or `gemini -v` from your terminal
- Check the globally installed version using your package manager:
- npm: `npm list -g @google/gemini-cli`
- pnpm: `pnpm list -g @google/gemini-cli`
- yarn: `yarn global list @google/gemini-cli`
- bun: `bun pm ls -g @google/gemini-cli`
- homebrew: `brew list --versions gemini-cli`
- Inside an active Gemini CLI session, use the `/about` command
### How do I update Gemini CLI to the latest version?
If you installed it globally via `npm`, update it using the command
+6 -36
View File
@@ -13,12 +13,7 @@ import {
} from './test-helper.js';
import fs from 'node:fs';
import path from 'node:path';
import {
DEFAULT_GEMINI_MODEL,
type ScriptItem,
extractFakeResponses,
extractUserPrompts,
} from '@google/gemini-cli-core';
import { DEFAULT_GEMINI_MODEL } from '@google/gemini-cli-core';
export interface AppEvalCase {
name: string;
@@ -28,12 +23,6 @@ export interface AppEvalCase {
files?: Record<string, string>;
setup?: (rig: AppRig) => Promise<void>;
assert: (rig: AppRig, output: string) => Promise<void>;
/**
* Optional script to "prime the pump" before the main prompt.
* A sequential array interleaving MockUserTurn (e.g., userText('hello'))
* and FakeResponse (e.g., mockGenerateContentStreamText('hi')).
*/
script?: ScriptItem[];
}
/**
@@ -42,25 +31,11 @@ export interface AppEvalCase {
*/
export function appEvalTest(policy: EvalPolicy, evalCase: AppEvalCase) {
const fn = async () => {
const configOverrides = {
model: DEFAULT_GEMINI_MODEL,
...evalCase.configOverrides,
};
let userPrompts: string[] = [];
if (evalCase.script) {
configOverrides.fakeModelConfig = {
responses: extractFakeResponses(evalCase.script),
hybridHandoff: true,
};
// Extract the sequence of user prompts for the Mock User driver
userPrompts = extractUserPrompts(evalCase.script);
}
const rig = new AppRig({
configOverrides,
configOverrides: {
model: DEFAULT_GEMINI_MODEL,
...evalCase.configOverrides,
},
});
const { logDir, sanitizedName } = await prepareLogDir(evalCase.name);
@@ -87,16 +62,11 @@ export function appEvalTest(policy: EvalPolicy, evalCase: AppEvalCase) {
}
// Render the app!
await rig.render();
rig.render();
// Wait for initial ready state
await rig.waitForIdle();
// Execute priming script if requested
if (userPrompts.length > 0) {
await rig.driveMockUser(userPrompts, evalCase.timeout);
}
// Send the initial prompt
await rig.sendMessage(evalCase.prompt);
-84
View File
@@ -1,84 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import path from 'node:path';
import fs from 'node:fs';
import { appEvalTest } from './app-test-helper.js';
import {
userText,
mockGenerateContentStreamText,
} from '@google/gemini-cli-core';
describe('Auto-Distillation Behavioral Evals', () => {
appEvalTest('USUALLY_PASSES', {
name: 'Agent successfully navigates truncated output using the structural map to extract a secret',
timeout: 180000,
configOverrides: { autoDistillation: true },
setup: async (rig) => {
const testDir = rig.getTestDir();
const mockData: any = {
system_info: {
version: '1.0.0',
uptime: 999999,
environment: 'production',
},
active_sessions: [],
quarantined_payloads: [
{ id: 'Subject-01', status: 'cleared' },
{
id: 'Subject-89',
secret_token: 'the_cake_is_a_lie',
status: 'held_for_review',
},
{ id: 'Subject-99', status: 'cleared' },
],
archived_metrics: [],
};
for (let i = 0; i < 300; i++) {
mockData.active_sessions.push({
session_id: `sess_${i.toString().padStart(4, '0')}`,
duration_ms: Math.floor(Math.random() * 10000),
bytes_transferred: Math.floor(Math.random() * 50000),
});
}
for (let i = 0; i < 2000; i++) {
mockData.archived_metrics.push({
timestamp: Date.now() - i * 1000,
cpu_load: parseFloat(Math.random().toFixed(4)),
mem_usage: parseFloat(Math.random().toFixed(4)),
});
}
const massiveString = JSON.stringify(mockData, null, 2);
fs.writeFileSync(
path.join(testDir, 'server_state_dump.json'),
massiveString,
);
},
script: [
userText(
'We have a critical error in production. Are you ready to help?',
),
mockGenerateContentStreamText(
'I am ready. Please provide the details of the error.',
),
],
prompt: `My application crashed with: "FATAL: Subject-89 held for review in quarantine". \n\nPlease run \`cat server_state_dump.json\` to investigate. The file is massive, so your tool output will be automatically truncated and you will receive a structural map instead. Use that structural map to determine the right command to extract the \`secret_token\` for Subject-89. Please state the exact secret token when you find it.`,
assert: async (rig) => {
await rig.drainBreakpointsUntilIdle(undefined, 60000);
const finalOutput = rig.getStaticOutput();
// Ensure the agent correctly extracted the secret token after navigating the distilled output
expect(finalOutput).toContain('the_cake_is_a_lie');
},
});
});
-38
View File
@@ -1,38 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { appEvalTest } from './app-test-helper.js';
import {
userText,
mockGenerateContentStreamText,
} from '@google/gemini-cli-core';
describe('Hybrid Handoff (Mock User to Live Model)', () => {
appEvalTest('ALWAYS_PASSES', {
name: 'Mock User successfully primes AppRig using a scripted history and hands off to live model',
timeout: 120000,
script: [
userText('Start priming'),
mockGenerateContentStreamText(
"Hello! I am a fake response. Let's prime the pump.",
),
userText('Continue priming'),
mockGenerateContentStreamText(
'Pump primed successfully. Ready for handoff.',
),
],
prompt: 'What is 5 * 5? Please answer with just the final number.',
assert: async (rig) => {
await rig.drainBreakpointsUntilIdle(undefined, 30000);
const liveOutput = rig.getStaticOutput();
// Ensure the handoff was successful
expect(liveOutput).toContain('25');
},
});
});
@@ -1,5 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you with that."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and check if there is a heading"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"new_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"success":true,"summary":"SUCCESS_POLICY_TEST_COMPLETED"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Task completed successfully. The page has the heading \"Example Domain\"."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]}
-178
View File
@@ -1,178 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig, poll } from './test-helper.js';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
import stripAnsi from 'strip-ansi';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const chromeAvailable = (() => {
try {
if (process.platform === 'darwin') {
execSync(
'test -d "/Applications/Google Chrome.app" || test -d "/Applications/Chromium.app"',
{
stdio: 'ignore',
},
);
} else if (process.platform === 'linux') {
execSync(
'which google-chrome || which chromium-browser || which chromium',
{ stdio: 'ignore' },
);
} else if (process.platform === 'win32') {
const chromePaths = [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
`${process.env['LOCALAPPDATA'] ?? ''}\\Google\\Chrome\\Application\\chrome.exe`,
];
const found = chromePaths.some((p) => existsSync(p));
if (!found) {
execSync('where chrome || where chromium', { stdio: 'ignore' });
}
} else {
return false;
}
return true;
} catch {
return false;
}
})();
describe.skipIf(!chromeAvailable)('browser-policy', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
await rig.cleanup();
});
it('should skip confirmation when "Allow all server tools for this session" is chosen', async () => {
rig.setup('browser-policy-skip-confirmation', {
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
settings: {
agents: {
overrides: {
browser_agent: {
enabled: true,
},
},
browser: {
headless: true,
sessionMode: 'isolated',
allowedDomains: ['example.com'],
},
},
},
});
// Manually trust the folder to avoid the dialog and enable option 3
const geminiDir = join(rig.homeDir!, '.gemini');
mkdirSync(geminiDir, { recursive: true });
// Write to trustedFolders.json
const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
const trustedFolders = {
[rig.testDir!]: 'TRUST_FOLDER',
};
writeFileSync(trustedFoldersPath, JSON.stringify(trustedFolders, null, 2));
// Force confirmation for browser agent.
// NOTE: We don't force confirm browser tools here because "Allow all server tools"
// adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
// a rule in the user tier (4.x) like the one from this TOML.
// By removing the explicit mcp rule, the first MCP tool will still prompt
// due to default approvalMode = 'default', and then "Allow all" will correctly
// bypass subsequent tools.
const policyFile = join(rig.testDir!, 'force-confirm.toml');
writeFileSync(
policyFile,
`
[[rule]]
name = "Force confirm browser_agent"
toolName = "browser_agent"
decision = "ask_user"
priority = 200
`,
);
// Update settings.json in both project and home directories to point to the policy file
for (const baseDir of [rig.testDir!, rig.homeDir!]) {
const settingsPath = join(baseDir, '.gemini', 'settings.json');
if (existsSync(settingsPath)) {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
settings.policyPaths = [policyFile];
// Ensure folder trust is enabled
settings.security = settings.security || {};
settings.security.folderTrust = settings.security.folderTrust || {};
settings.security.folderTrust.enabled = true;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
}
}
const run = await rig.runInteractive({
approvalMode: 'default',
env: {
GEMINI_CLI_INTEGRATION_TEST: 'true',
},
});
await run.sendKeys(
'Open https://example.com and check if there is a heading\r',
);
await run.sendKeys('\r');
// Handle confirmations.
// 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
await poll(
() => stripAnsi(run.output).toLowerCase().includes('action required'),
60000,
1000,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 2000));
// Handle privacy notice
await poll(
() => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
5000,
100,
);
await run.sendKeys('1\r');
await new Promise((r) => setTimeout(r, 5000));
// new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
await poll(
() => {
const stripped = stripAnsi(run.output).toLowerCase();
return (
stripped.includes('new_page') &&
stripped.includes('allow all server tools for this session')
);
},
60000,
1000,
);
// Select "Allow all server tools for this session" (option 3)
await run.sendKeys('3\r');
await new Promise((r) => setTimeout(r, 30000));
const output = stripAnsi(run.output).toLowerCase();
expect(output).toContain('browser_agent');
expect(output).toContain('completed successfully');
});
});
+5 -5
View File
@@ -22,7 +22,7 @@
"gemini": "bundle/gemini.js"
},
"devDependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
"@agentclientprotocol/sdk": "^0.12.0",
"@octokit/rest": "^22.0.0",
"@types/marked": "^5.0.2",
"@types/mime-types": "^3.0.1",
@@ -84,9 +84,9 @@
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.16.1.tgz",
"integrity": "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw==",
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.12.0.tgz",
"integrity": "sha512-V8uH/KK1t7utqyJmTA7y7DzKu6+jKFIXM+ZVouz8E55j8Ej2RV42rEvPKn3/PpBJlliI5crcGk1qQhZ7VwaepA==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
@@ -17531,7 +17531,7 @@
"version": "0.36.0-nightly.20260317.2f90b4653",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
"@agentclientprotocol/sdk": "^0.12.0",
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
+1 -1
View File
@@ -87,7 +87,7 @@
"LICENSE"
],
"devDependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
"@agentclientprotocol/sdk": "^0.12.0",
"@octokit/rest": "^22.0.0",
"@types/marked": "^5.0.2",
"@types/mime-types": "^3.0.1",
+1 -1
View File
@@ -30,7 +30,7 @@
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.36.0-nightly.20260317.2f90b4653"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
"@agentclientprotocol/sdk": "^0.12.0",
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
+8 -92
View File
@@ -177,9 +177,6 @@ describe('GeminiAgent', () => {
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
get config() {
return this;
},
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
mockSettings = {
merged: {
@@ -551,7 +548,7 @@ describe('GeminiAgent', () => {
});
expect(session.prompt).toHaveBeenCalled();
expect(result).toMatchObject({ stopReason: 'end_turn' });
expect(result).toEqual({ stopReason: 'end_turn' });
});
it('should delegate setMode to session', async () => {
@@ -659,12 +656,6 @@ describe('Session', () => {
getGitService: vi.fn().mockResolvedValue({} as GitService),
waitForMcpInit: vi.fn(),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
get config() {
return this;
},
get toolRegistry() {
return mockToolRegistry;
},
} as unknown as Mocked<Config>;
mockConnection = {
sessionUpdate: vi.fn(),
@@ -750,7 +741,7 @@ describe('Session', () => {
content: { type: 'text', text: 'Hello' },
},
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
expect(result).toEqual({ stopReason: 'end_turn' });
});
it('should handle /memory command', async () => {
@@ -767,7 +758,7 @@ describe('Session', () => {
prompt: [{ type: 'text', text: '/memory view' }],
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
expect(result).toEqual({ stopReason: 'end_turn' });
expect(handleCommandSpy).toHaveBeenCalledWith(
'/memory view',
expect.any(Object),
@@ -789,7 +780,7 @@ describe('Session', () => {
prompt: [{ type: 'text', text: '/extensions list' }],
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
expect(result).toEqual({ stopReason: 'end_turn' });
expect(handleCommandSpy).toHaveBeenCalledWith(
'/extensions list',
expect.any(Object),
@@ -811,7 +802,7 @@ describe('Session', () => {
prompt: [{ type: 'text', text: '/extensions explore' }],
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
expect(result).toEqual({ stopReason: 'end_turn' });
expect(handleCommandSpy).toHaveBeenCalledWith(
'/extensions explore',
expect.any(Object),
@@ -833,7 +824,7 @@ describe('Session', () => {
prompt: [{ type: 'text', text: '/restore' }],
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
expect(result).toEqual({ stopReason: 'end_turn' });
expect(handleCommandSpy).toHaveBeenCalledWith(
'/restore',
expect.any(Object),
@@ -855,7 +846,7 @@ describe('Session', () => {
prompt: [{ type: 'text', text: '/init' }],
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
expect(result).toEqual({ stopReason: 'end_turn' });
expect(handleCommandSpy).toHaveBeenCalledWith('/init', expect.any(Object));
expect(mockChat.sendMessageStream).not.toHaveBeenCalled();
});
@@ -903,13 +894,10 @@ describe('Session', () => {
update: expect.objectContaining({
sessionUpdate: 'tool_call_update',
status: 'completed',
title: 'Test Tool',
locations: [],
kind: 'read',
}),
}),
);
expect(result).toMatchObject({ stopReason: 'end_turn' });
expect(result).toEqual({ stopReason: 'end_turn' });
});
it('should handle tool call permission request', async () => {
@@ -1318,18 +1306,6 @@ describe('Session', () => {
expect(path.resolve).toHaveBeenCalled();
expect(fs.stat).toHaveBeenCalled();
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
sessionUpdate: 'tool_call_update',
status: 'completed',
title: 'Read files',
locations: [],
kind: 'read',
}),
}),
);
// Verify ReadManyFilesTool was used (implicitly by checking if sendMessageStream was called with resolved content)
// Since we mocked ReadManyFilesTool to return specific content, we can check the args passed to sendMessageStream
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
@@ -1345,65 +1321,6 @@ describe('Session', () => {
);
});
it('should handle @path resolution error', async () => {
(path.resolve as unknown as Mock).mockReturnValue('/tmp/error.txt');
(fs.stat as unknown as Mock).mockResolvedValue({
isDirectory: () => false,
});
(isWithinRoot as unknown as Mock).mockReturnValue(true);
const MockReadManyFilesTool = ReadManyFilesTool as unknown as Mock;
MockReadManyFilesTool.mockImplementationOnce(() => ({
name: 'read_many_files',
kind: 'read',
build: vi.fn().mockReturnValue({
getDescription: () => 'Read files',
toolLocations: () => [],
execute: vi.fn().mockRejectedValue(new Error('File read failed')),
}),
}));
const stream = createMockStream([
{
type: StreamEventType.CHUNK,
value: { candidates: [] },
},
]);
mockChat.sendMessageStream.mockResolvedValue(stream);
await expect(
session.prompt({
sessionId: 'session-1',
prompt: [
{ type: 'text', text: 'Read' },
{
type: 'resource_link',
uri: 'file://error.txt',
mimeType: 'text/plain',
name: 'error.txt',
},
],
}),
).rejects.toThrow('File read failed');
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
sessionUpdate: 'tool_call_update',
status: 'failed',
content: expect.arrayContaining([
expect.objectContaining({
content: expect.objectContaining({
text: expect.stringMatching(/File read failed/),
}),
}),
]),
kind: 'read',
}),
}),
);
});
it('should handle cancellation during prompt', async () => {
let streamController: ReadableStreamDefaultController<unknown>;
const stream = new ReadableStream({
@@ -1517,7 +1434,6 @@ describe('Session', () => {
content: expect.objectContaining({ text: 'Tool failed' }),
}),
]),
kind: 'read',
}),
}),
);
+36 -113
View File
@@ -47,7 +47,6 @@ import {
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
getDisplayString,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import * as acp from '@agentclientprotocol/sdk';
import { AcpFileSystemService } from './fileSystemService.js';
@@ -105,7 +104,7 @@ export class GeminiAgent {
private customHeaders: Record<string, string> | undefined;
constructor(
private context: AgentLoopContext,
private config: Config,
private settings: LoadedSettings,
private argv: CliArgs,
private connection: acp.AgentSideConnection,
@@ -149,7 +148,7 @@ export class GeminiAgent {
},
];
await this.context.config.initialize();
await this.config.initialize();
const version = await getVersion();
return {
protocolVersion: acp.PROTOCOL_VERSION,
@@ -221,7 +220,7 @@ export class GeminiAgent {
this.baseUrl = baseUrl;
this.customHeaders = headers;
await this.context.config.refreshAuth(
await this.config.refreshAuth(
method,
apiKey ?? this.apiKey,
baseUrl,
@@ -538,7 +537,7 @@ export class Session {
constructor(
private readonly id: string,
private readonly chat: GeminiChat,
private readonly context: AgentLoopContext,
private readonly config: Config,
private readonly connection: acp.AgentSideConnection,
private readonly settings: LoadedSettings,
) {}
@@ -553,15 +552,13 @@ export class Session {
}
setMode(modeId: acp.SessionModeId): acp.SetSessionModeResponse {
const availableModes = buildAvailableModes(
this.context.config.isPlanEnabled(),
);
const availableModes = buildAvailableModes(this.config.isPlanEnabled());
const mode = availableModes.find((m) => m.id === modeId);
if (!mode) {
throw new Error(`Invalid or unavailable mode: ${modeId}`);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
this.context.config.setApprovalMode(mode.id as ApprovalMode);
this.config.setApprovalMode(mode.id as ApprovalMode);
return {};
}
@@ -582,7 +579,7 @@ export class Session {
}
setModel(modelId: acp.ModelId): acp.SetSessionModelResponse {
this.context.config.setModel(modelId);
this.config.setModel(modelId);
return {};
}
@@ -637,7 +634,7 @@ export class Session {
}
}
const tool = this.context.toolRegistry.getTool(toolCall.name);
const tool = this.config.getToolRegistry().getTool(toolCall.name);
await this.sendUpdate({
sessionUpdate: 'tool_call',
@@ -661,7 +658,7 @@ export class Session {
const pendingSend = new AbortController();
this.pendingPrompt = pendingSend;
await this.context.config.waitForMcpInit();
await this.config.waitForMcpInit();
const promptId = Math.random().toString(16).slice(2);
const chat = this.chat;
@@ -699,22 +696,10 @@ export class Session {
// It uses `parts` argument but effectively ignores it in current implementation
const handled = await this.handleCommand(commandText, parts);
if (handled) {
return {
stopReason: 'end_turn',
_meta: {
quota: {
token_count: { input_tokens: 0, output_tokens: 0 },
model_usage: [],
},
},
};
return { stopReason: 'end_turn' };
}
}
let totalInputTokens = 0;
let totalOutputTokens = 0;
const modelUsageMap = new Map<string, { input: number; output: number }>();
let nextMessage: Content | null = { role: 'user', parts };
while (nextMessage !== null) {
@@ -727,8 +712,8 @@ export class Session {
try {
const model = resolveModel(
this.context.config.getModel(),
(await this.context.config.getGemini31Launched?.()) ?? false,
this.config.getModel(),
(await this.config.getGemini31Launched?.()) ?? false,
);
const responseStream = await chat.sendMessageStream(
{ model },
@@ -739,25 +724,11 @@ export class Session {
);
nextMessage = null;
let turnInputTokens = 0;
let turnOutputTokens = 0;
let turnModelId = model;
for await (const resp of responseStream) {
if (pendingSend.signal.aborted) {
return { stopReason: CoreToolCallStatus.Cancelled };
}
if (resp.type === StreamEventType.CHUNK && resp.value.usageMetadata) {
turnInputTokens =
resp.value.usageMetadata.promptTokenCount ?? turnInputTokens;
turnOutputTokens =
resp.value.usageMetadata.candidatesTokenCount ?? turnOutputTokens;
if (resp.value.modelVersion) {
turnModelId = resp.value.modelVersion;
}
}
if (
resp.type === StreamEventType.CHUNK &&
resp.value.candidates &&
@@ -789,19 +760,6 @@ export class Session {
}
}
totalInputTokens += turnInputTokens;
totalOutputTokens += turnOutputTokens;
if (turnInputTokens > 0 || turnOutputTokens > 0) {
const existing = modelUsageMap.get(turnModelId) ?? {
input: 0,
output: 0,
};
existing.input += turnInputTokens;
existing.output += turnOutputTokens;
modelUsageMap.set(turnModelId, existing);
}
if (pendingSend.signal.aborted) {
return { stopReason: CoreToolCallStatus.Cancelled };
}
@@ -838,28 +796,7 @@ export class Session {
}
}
const modelUsageArray = Array.from(modelUsageMap.entries()).map(
([modelName, counts]) => ({
model: modelName,
token_count: {
input_tokens: counts.input,
output_tokens: counts.output,
},
}),
);
return {
stopReason: 'end_turn',
_meta: {
quota: {
token_count: {
input_tokens: totalInputTokens,
output_tokens: totalOutputTokens,
},
model_usage: modelUsageArray,
},
},
};
return { stopReason: 'end_turn' };
}
private async handleCommand(
@@ -867,9 +804,9 @@ export class Session {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
parts: Part[],
): Promise<boolean> {
const gitService = await this.context.config.getGitService();
const gitService = await this.config.getGitService();
const commandContext = {
agentContext: this.context,
config: this.config,
settings: this.settings,
git: gitService,
sendMessage: async (text: string) => {
@@ -905,7 +842,7 @@ export class Session {
const errorResponse = (error: Error) => {
const durationMs = Date.now() - startTime;
logToolCall(
this.context.config,
this.config,
new ToolCallEvent(
undefined,
fc.name ?? '',
@@ -935,7 +872,7 @@ export class Session {
return errorResponse(new Error('Missing function name'));
}
const toolRegistry = this.context.toolRegistry;
const toolRegistry = this.config.getToolRegistry();
const tool = toolRegistry.getTool(fc.name);
if (!tool) {
@@ -971,10 +908,7 @@ export class Session {
const params: acp.RequestPermissionRequest = {
sessionId: this.id,
options: toPermissionOptions(
confirmationDetails,
this.context.config,
),
options: toPermissionOptions(confirmationDetails, this.config),
toolCall: {
toolCallId: callId,
status: 'pending',
@@ -1032,15 +966,12 @@ export class Session {
sessionUpdate: 'tool_call_update',
toolCallId: callId,
status: 'completed',
title: invocation.getDescription(),
content: content ? [content] : [],
locations: invocation.toolLocations(),
kind: toAcpToolKind(tool.kind),
});
const durationMs = Date.now() - startTime;
logToolCall(
this.context.config,
this.config,
new ToolCallEvent(
undefined,
fc.name ?? '',
@@ -1054,7 +985,7 @@ export class Session {
),
);
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
{
status: CoreToolCallStatus.Success,
request: {
@@ -1072,8 +1003,8 @@ export class Session {
fc.name,
callId,
toolResult.llmContent,
this.context.config.getActiveModel(),
this.context.config,
this.config.getActiveModel(),
this.config,
),
resultDisplay: toolResult.returnDisplay,
error: undefined,
@@ -1086,8 +1017,8 @@ export class Session {
fc.name,
callId,
toolResult.llmContent,
this.context.config.getActiveModel(),
this.context.config,
this.config.getActiveModel(),
this.config,
);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
@@ -1099,10 +1030,9 @@ export class Session {
content: [
{ type: 'content', content: { type: 'text', text: error.message } },
],
kind: toAcpToolKind(tool.kind),
});
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
{
status: CoreToolCallStatus.Error,
request: {
@@ -1188,18 +1118,18 @@ export class Session {
const atPathToResolvedSpecMap = new Map<string, string>();
// Get centralized file discovery service
const fileDiscovery = this.context.config.getFileService();
const fileDiscovery = this.config.getFileService();
const fileFilteringOptions: FilterFilesOptions =
this.context.config.getFileFilteringOptions();
this.config.getFileFilteringOptions();
const pathSpecsToRead: string[] = [];
const contentLabelsForDisplay: string[] = [];
const ignoredPaths: string[] = [];
const toolRegistry = this.context.toolRegistry;
const toolRegistry = this.config.getToolRegistry();
const readManyFilesTool = new ReadManyFilesTool(
this.context.config,
this.context.messageBus,
this.config,
this.config.getMessageBus(),
);
const globTool = toolRegistry.getTool('glob');
@@ -1218,11 +1148,8 @@ export class Session {
let currentPathSpec = pathName;
let resolvedSuccessfully = false;
try {
const absolutePath = path.resolve(
this.context.config.getTargetDir(),
pathName,
);
if (isWithinRoot(absolutePath, this.context.config.getTargetDir())) {
const absolutePath = path.resolve(this.config.getTargetDir(), pathName);
if (isWithinRoot(absolutePath, this.config.getTargetDir())) {
const stats = await fs.stat(absolutePath);
if (stats.isDirectory()) {
currentPathSpec = pathName.endsWith('/')
@@ -1242,7 +1169,7 @@ export class Session {
}
} catch (error) {
if (isNodeError(error) && error.code === 'ENOENT') {
if (this.context.config.getEnableRecursiveFileSearch() && globTool) {
if (this.config.getEnableRecursiveFileSearch() && globTool) {
this.debug(
`Path ${pathName} not found directly, attempting glob search.`,
);
@@ -1250,7 +1177,7 @@ export class Session {
const globResult = await globTool.buildAndExecute(
{
pattern: `**/*${pathName}*`,
path: this.context.config.getTargetDir(),
path: this.config.getTargetDir(),
},
abortSignal,
);
@@ -1264,7 +1191,7 @@ export class Session {
if (lines.length > 1 && lines[1]) {
const firstMatchAbsolute = lines[1].trim();
currentPathSpec = path.relative(
this.context.config.getTargetDir(),
this.config.getTargetDir(),
firstMatchAbsolute,
);
this.debug(
@@ -1397,10 +1324,7 @@ export class Session {
sessionUpdate: 'tool_call_update',
toolCallId: callId,
status: 'completed',
title: invocation.getDescription(),
content: content ? [content] : [],
locations: invocation.toolLocations(),
kind: toAcpToolKind(readManyFilesTool.kind),
});
if (Array.isArray(result.llmContent)) {
const fileContentRegex = /^--- (.*?) ---\n\n([\s\S]*?)\n\n$/;
@@ -1444,7 +1368,6 @@ export class Session {
},
},
],
kind: toAcpToolKind(readManyFilesTool.kind),
});
throw error;
@@ -1479,7 +1402,7 @@ export class Session {
}
debug(msg: string) {
if (this.context.config.getDebugMode()) {
if (this.config.getDebugMode()) {
debugLogger.warn(msg);
}
}
+2 -6
View File
@@ -97,9 +97,6 @@ describe('GeminiAgent Session Resume', () => {
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
get config() {
return this;
},
} as unknown as Mocked<Config>;
mockSettings = {
merged: {
@@ -161,10 +158,9 @@ describe('GeminiAgent Session Resume', () => {
],
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockConfig as any).toolRegistry = {
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
getTool: vi.fn().mockReturnValue({ kind: 'read' }),
};
});
(SessionSelector as unknown as Mock).mockImplementation(() => ({
resolveSession: vi.fn().mockResolvedValue({
+9 -10
View File
@@ -53,7 +53,7 @@ export class ListExtensionsCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const extensions = listExtensions(context.agentContext.config);
const extensions = listExtensions(context.config);
const data = extensions.length ? extensions : 'No extensions installed.';
return { name: this.name, data };
@@ -134,7 +134,7 @@ export class EnableExtensionCommand implements Command {
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.agentContext.config,
context.config,
args,
'enable',
);
@@ -156,8 +156,7 @@ export class EnableExtensionCommand implements Command {
if (extension?.mcpServers) {
const mcpEnablementManager = McpServerEnablementManager.getInstance();
const mcpClientManager =
context.agentContext.config.getMcpClientManager();
const mcpClientManager = context.config.getMcpClientManager();
const enabledServers = await mcpEnablementManager.autoEnableServers(
Object.keys(extension.mcpServers),
);
@@ -192,7 +191,7 @@ export class DisableExtensionCommand implements Command {
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.agentContext.config,
context.config,
args,
'disable',
);
@@ -224,7 +223,7 @@ export class InstallExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.agentContext.config.getExtensionLoader();
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -269,7 +268,7 @@ export class LinkExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.agentContext.config.getExtensionLoader();
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -314,7 +313,7 @@ export class UninstallExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.agentContext.config.getExtensionLoader();
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -370,7 +369,7 @@ export class RestartExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.agentContext.config.getExtensionLoader();
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot restart extensions.' };
}
@@ -425,7 +424,7 @@ export class UpdateExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.agentContext.config.getExtensionLoader();
const extensionLoader = context.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot update extensions.' };
}
+1 -1
View File
@@ -22,7 +22,7 @@ export class InitCommand implements Command {
context: CommandContext,
_args: string[] = [],
): Promise<CommandExecutionResponse> {
const targetDir = context.agentContext.config.getTargetDir();
const targetDir = context.config.getTargetDir();
if (!targetDir) {
throw new Error('Command requires a workspace.');
}
+6 -6
View File
@@ -49,7 +49,7 @@ export class ShowMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = showMemory(context.agentContext.config);
const result = showMemory(context.config);
return { name: this.name, data: result.content };
}
}
@@ -63,7 +63,7 @@ export class RefreshMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = await refreshMemory(context.agentContext.config);
const result = await refreshMemory(context.config);
return { name: this.name, data: result.content };
}
}
@@ -76,7 +76,7 @@ export class ListMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = listMemoryFiles(context.agentContext.config);
const result = listMemoryFiles(context.config);
return { name: this.name, data: result.content };
}
}
@@ -95,7 +95,7 @@ export class AddMemoryCommand implements Command {
return { name: this.name, data: result.content };
}
const toolRegistry = context.agentContext.toolRegistry;
const toolRegistry = context.config.getToolRegistry();
const tool = toolRegistry.getTool(result.toolName);
if (tool) {
const abortController = new AbortController();
@@ -106,10 +106,10 @@ export class AddMemoryCommand implements Command {
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
shellExecutionConfig: {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: context.agentContext.sandboxManager,
sandboxManager: context.config.sandboxManager,
},
});
await refreshMemory(context.agentContext.config);
await refreshMemory(context.config);
return {
name: this.name,
data: `Added memory: "${textToAdd}"`,
+2 -3
View File
@@ -29,8 +29,7 @@ export class RestoreCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const { agentContext: agentContext, git: gitService } = context;
const { config } = agentContext;
const { config, git: gitService } = context;
const argsStr = args.join(' ');
try {
@@ -117,7 +116,7 @@ export class ListCheckpointsCommand implements Command {
readonly description = 'Lists all available checkpoints.';
async execute(context: CommandContext): Promise<CommandExecutionResponse> {
const { config } = context.agentContext;
const { config } = context;
try {
if (!config.getCheckpointingEnabled()) {
+2 -2
View File
@@ -4,11 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { AgentLoopContext, GitService } from '@google/gemini-cli-core';
import type { Config, GitService } from '@google/gemini-cli-core';
import type { LoadedSettings } from '../../config/settings.js';
export interface CommandContext {
agentContext: AgentLoopContext;
config: Config;
settings: LoadedSettings;
git?: GitService;
sendMessage: (text: string) => Promise<void>;
+1 -1
View File
@@ -14,7 +14,7 @@ export class AcpFileSystemService implements FileSystemService {
constructor(
private readonly connection: acp.AgentSideConnection,
private readonly sessionId: string,
private readonly capabilities: acp.FileSystemCapabilities,
private readonly capabilities: acp.FileSystemCapability,
private readonly fallback: FileSystemService,
) {}
-2
View File
@@ -840,8 +840,6 @@ export async function loadCliConfig(
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
autoDistillation: settings.experimental?.autoDistillation,
experimentalMemoryManager: settings.experimental?.memoryManager,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
toolOutputMasking: settings.experimental?.toolOutputMasking,
@@ -516,9 +516,7 @@ describe('Policy Engine Integration Tests', () => {
);
expect(mcpServerRule?.priority).toBe(4.1); // MCP allowed server
const readOnlyToolRule = rules.find(
(r) => r.toolName === 'glob' && !r.subagent,
);
const readOnlyToolRule = rules.find((r) => r.toolName === 'glob');
// Priority 70 in default tier → 1.07 (Overriding Plan Mode Deny)
expect(readOnlyToolRule?.priority).toBeCloseTo(1.07, 5);
@@ -675,7 +673,7 @@ describe('Policy Engine Integration Tests', () => {
const server1Rule = rules.find((r) => r.toolName === 'mcp_server1_*');
expect(server1Rule?.priority).toBe(4.1); // Allowed servers (user tier)
const globRule = rules.find((r) => r.toolName === 'glob' && !r.subagent);
const globRule = rules.find((r) => r.toolName === 'glob');
// Priority 70 in default tier → 1.07
expect(globRule?.priority).toBeCloseTo(1.07, 5); // Auto-accept read-only
-19
View File
@@ -1870,15 +1870,6 @@ const SETTINGS_SCHEMA = {
description: 'Enable local and remote subagents.',
showInDialog: false,
},
autoDistillation: {
type: 'boolean',
label: 'Auto Distillation',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable automatic distillation for large tool outputs.',
showInDialog: false,
},
extensionManagement: {
type: 'boolean',
label: 'Extension Management',
@@ -2054,16 +2045,6 @@ const SETTINGS_SCHEMA = {
},
},
},
memoryManager: {
type: 'boolean',
label: 'Memory Manager Agent',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
showInDialog: true,
},
topicUpdateNarration: {
type: 'boolean',
label: 'Topic & Update Narration',
@@ -1,73 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Auto-distillation Integration > should truncate and summarize massive tool outputs, and we should golden the chat history 1`] = `
[
{
"parts": [
{
"text": "<SESSION_CONTEXT>",
},
],
"role": "user",
},
{
"parts": [
{
"text": "Fetch the massive file.",
},
],
"role": "user",
},
{
"parts": [
{
"text": "I will now fetch the data.",
},
{
"functionCall": {
"args": {
"command": "cat large.txt",
},
"id": "<CALL_ID>",
"name": "run_shell_command",
},
},
],
"role": "model",
},
{
"parts": [
{
"functionResponse": {
"id": "<CALL_ID>",
"name": "run_shell_command",
"response": {
"output": "Output too large. Showing first 10 and last 40 characters. For full output see: <TEST_DIR>/.gemini/tmp/<APP_RIG_ID>/tool-outputs/session-<SESSION_ID>/run_shell_command_<TIMESTAMP>_<INDEX>.txt
Output: ca
... [39 characters omitted] ...
Exit Code: 1
Process Group PGID: <PGID>
--- Structural Map of Truncated Content ---
- Line 1: Header
- Lines 2-5000: User data
- Line 5001: Footer",
},
},
},
],
"role": "user",
},
{
"parts": [
{
"text": "I got the summarized output. Task complete.",
},
],
"role": "model",
},
]
`;
@@ -1,68 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { AppRig } from '../test-utils/AppRig.js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { FakeContentGenerator } from '@google/gemini-cli-core';
import { PolicyDecision } from '@google/gemini-cli-core';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('Auto-distillation Integration', () => {
let rig: AppRig | undefined;
afterEach(async () => {
if (rig) {
await rig.unmount();
}
vi.restoreAllMocks();
});
it('should truncate and summarize massive tool outputs, and we should golden the chat history', async () => {
const fakeResponsesPath = path.join(
__dirname,
'../test-utils/fixtures/auto-distillation.responses',
);
const contentGenerator =
await FakeContentGenerator.fromFile(fakeResponsesPath);
rig = new AppRig({
contentGenerator,
configOverrides: { autoDistillation: true },
});
await rig.initialize();
const config = rig.getConfig();
// 50 chars threshold. > 75 chars triggers summarization
vi.spyOn(config, 'getTruncateToolOutputThreshold').mockReturnValue(50);
rig.setToolPolicy('run_shell_command', PolicyDecision.ASK_USER);
rig.setMockCommands([
{
command: /cat large.txt/,
result: {
output: 'A'.repeat(100),
exitCode: 0,
},
},
]);
await rig.render();
await rig.waitForIdle();
await rig.sendMessage('Fetch the massive file.');
await rig.waitForOutput('Shell');
await rig.resolveTool('Shell');
await rig.waitForOutput('Task complete.');
expect(rig.getCuratedHistory()).toMatchSnapshot();
});
});
@@ -1,78 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { AppRig } from '../test-utils/AppRig.js';
import {
FakeContentGenerator,
FallbackContentGenerator,
userText,
mockGenerateContentStreamText,
extractUserPrompts,
extractFakeResponses,
type ScriptItem,
} from '@google/gemini-cli-core';
describe('Hybrid Handoff (Mock User to Synthetic Live Model)', () => {
it('successfully transitions from mock responses to live responses', async () => {
// 1. Define the conversational script for the priming phase
const primingScript: ScriptItem[] = [
userText('Start priming'),
mockGenerateContentStreamText('Hello! I am a fake response.'),
userText('Continue priming'),
mockGenerateContentStreamText(
'Pump primed successfully. Ready for handoff.',
),
];
// 2. Setup the primary fake generator that runs through the priming script
const fakeGenerator = new FakeContentGenerator(
extractFakeResponses(primingScript),
);
// 3. Setup a "live" fallback generator (it's synthetic so we don't need API keys)
const mockLiveFallback = new FakeContentGenerator([
mockGenerateContentStreamText('The answer is 4.'),
]);
// We need countTokens so AppRig doesn't hang checking size during truncation
mockLiveFallback.countTokens = async () => ({ totalTokens: 10 });
// 4. Compose them using FallbackContentGenerator
const composedGenerator = new FallbackContentGenerator(
fakeGenerator,
mockLiveFallback,
);
// 5. Mount the AppRig natively supporting custom content generators
const rig = new AppRig({
contentGenerator: composedGenerator,
configOverrides: {
fakeResponses: [], // ensure it avoids disk IO attempts internally
},
});
await rig.initialize();
await rig.render();
await rig.waitForIdle();
// 6. Drive the Mock User sequence using the extracted prompts from the script
await rig.driveMockUser(extractUserPrompts(primingScript), 10000);
// 7. Send the final prompt that should exhaust the primary generator and trigger the fallback
await rig.sendMessage('What is 2 + 2?');
// 8. Wait for the fallback response to render
await rig.waitForOutput('The answer is 4.', 10000);
const output = rig.getStaticOutput();
expect(output).toContain('The answer is 4.');
// Wait for everything to settle so React act() warnings don't fire during unmount
await rig.drainBreakpointsUntilIdle(undefined, 10000);
await rig.unmount();
});
});
@@ -8,7 +8,7 @@ import { describe, it, afterEach } from 'vitest';
import { AppRig } from '../test-utils/AppRig.js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { FakeContentGenerator, PolicyDecision } from '@google/gemini-cli-core';
import { PolicyDecision } from '@google/gemini-cli-core';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -24,14 +24,12 @@ describe('Model Steering Integration', () => {
__dirname,
'../test-utils/fixtures/steering.responses',
);
const contentGenerator =
await FakeContentGenerator.fromFile(fakeResponsesPath);
rig = new AppRig({
contentGenerator,
fakeResponsesPath,
configOverrides: { modelSteering: true },
});
await rig.initialize();
await rig.render();
rig.render();
await rig.waitForIdle();
rig.setToolPolicy('list_directory', PolicyDecision.ASK_USER);
@@ -65,9 +65,9 @@ export const handleSlashCommand = async (
const logger = new Logger(config?.getSessionId() || '', config?.storage);
const commandContext: CommandContext = {
const context: CommandContext = {
services: {
agentContext: config,
config,
settings,
git: undefined,
logger,
@@ -84,7 +84,7 @@ export const handleSlashCommand = async (
},
};
const result = await commandToExecute.action(commandContext, args);
const result = await commandToExecute.action(context, args);
if (result) {
switch (result.type) {
@@ -31,14 +31,11 @@ describe('AtFileProcessor', () => {
mockConfig = {
// The processor only passes the config through, so we don't need a full mock.
get config() {
return this;
},
} as unknown as Config;
context = createMockCommandContext({
services: {
agentContext: mockConfig,
config: mockConfig,
},
});
@@ -63,7 +60,7 @@ describe('AtFileProcessor', () => {
const prompt: PartUnion[] = [{ text: 'Analyze @{file.txt}' }];
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
const result = await processor.process(prompt, contextWithoutConfig);
@@ -25,7 +25,7 @@ export class AtFileProcessor implements IPromptProcessor {
input: PromptPipelineContent,
context: CommandContext,
): Promise<PromptPipelineContent> {
const config = context.services.agentContext?.config;
const config = context.services.config;
if (!config) {
return input;
}
@@ -89,9 +89,6 @@ describe('ShellProcessor', () => {
getPolicyEngine: vi.fn().mockReturnValue({
check: mockPolicyEngineCheck,
}),
get config() {
return this as unknown as Config;
},
};
context = createMockCommandContext({
@@ -101,7 +98,7 @@ describe('ShellProcessor', () => {
args: 'default args',
},
services: {
agentContext: mockConfig as Config,
config: mockConfig as Config,
},
session: {
sessionShellAllowlist: new Set(),
@@ -123,7 +120,7 @@ describe('ShellProcessor', () => {
const prompt: PromptPipelineContent = createPromptPipelineContent('!{ls}');
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
@@ -74,7 +74,7 @@ export class ShellProcessor implements IPromptProcessor {
];
}
const config = context.services.agentContext?.config;
const config = context.services.config;
if (!config) {
throw new Error(
`Security configuration not loaded. Cannot verify shell command permissions for '${this.commandName}'. Aborting.`,
+11 -9
View File
@@ -5,10 +5,11 @@
*/
import { describe, it, afterEach, expect } from 'vitest';
import { act } from 'react';
import { AppRig } from './AppRig.js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { FakeContentGenerator, debugLogger } from '@google/gemini-cli-core';
import { debugLogger } from '@google/gemini-cli-core';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -25,14 +26,12 @@ describe('AppRig', () => {
'fixtures',
'steering.responses',
);
const contentGenerator =
await FakeContentGenerator.fromFile(fakeResponsesPath);
rig = new AppRig({
contentGenerator,
fakeResponsesPath,
configOverrides: { modelSteering: true },
});
await rig.initialize();
await rig.render();
rig.render();
await rig.waitForIdle();
// Set breakpoints on the canonical tool names
@@ -68,11 +67,14 @@ describe('AppRig', () => {
'fixtures',
'simple.responses',
);
const contentGenerator =
await FakeContentGenerator.fromFile(fakeResponsesPath);
rig = new AppRig({ contentGenerator });
rig = new AppRig({ fakeResponsesPath });
await rig.initialize();
await rig.render();
await act(async () => {
rig!.render();
// Allow async initializations (like banners) to settle within the act boundary
await new Promise((resolve) => setTimeout(resolve, 0));
});
// Wait for initial render
await rig.waitForIdle();
+38 -171
View File
@@ -11,7 +11,7 @@ import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { AppContainer } from '../ui/AppContainer.js';
import { renderWithProviders, type RenderInstance } from './render.js';
import { renderWithProviders } from './render.js';
import {
makeFakeConfig,
type Config,
@@ -31,8 +31,6 @@ import {
debugLogger,
CoreToolCallStatus,
IntegrityDataStatus,
ConsecaSafetyChecker,
type ContentGenerator,
} from '@google/gemini-cli-core';
import {
type MockShellCommand,
@@ -50,42 +48,37 @@ import type {
TrackedCompletedToolCall,
TrackedToolCall,
} from '../ui/hooks/useToolScheduler.js';
import type { Content, GenerateContentParameters } from '@google/genai';
// Global state observer for React-based signals
const sessionStateMap = new Map<string, StreamingState>();
const activeRigs = new Map<string, AppRig>();
// Mock useGeminiStream to report state changes back to the observer
vi.mock('../ui/hooks/useGeminiStream.js', async (importOriginal) => {
// Mock StreamingContext to report state changes back to the observer
vi.mock('../ui/contexts/StreamingContext.js', async (importOriginal) => {
const original =
await importOriginal<typeof import('../ui/hooks/useGeminiStream.js')>();
const React = await import('react');
await importOriginal<typeof import('../ui/contexts/StreamingContext.js')>();
const { useConfig } = await import('../ui/contexts/ConfigContext.js');
const React = await import('react');
return {
...original,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useGeminiStream: (...args: any[]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = (original.useGeminiStream as any)(...args);
useStreamingContext: () => {
const state = original.useStreamingContext();
const config = useConfig();
const sessionId = config.getSessionId();
React.useEffect(() => {
if (sessionId) {
sessionStateMap.set(sessionId, result.streamingState);
// If we see activity, we are no longer "awaiting" the start of a response
if (result.streamingState !== StreamingState.Idle) {
const rig = activeRigs.get(sessionId);
if (rig) {
rig.awaitingResponse = false;
}
sessionStateMap.set(sessionId, state);
// If we see activity, we are no longer "awaiting" the start of a response
if (state !== StreamingState.Idle) {
const rig = activeRigs.get(sessionId);
if (rig) {
rig.awaitingResponse = false;
}
}
}, [sessionId, result.streamingState]);
}, [sessionId, state]);
return result;
return state;
},
};
});
@@ -149,10 +142,10 @@ vi.mock('../ui/components/GeminiRespondingSpinner.js', async () => {
});
export interface AppRigOptions {
fakeResponsesPath?: string;
terminalWidth?: number;
terminalHeight?: number;
configOverrides?: Partial<ConfigParameters>;
contentGenerator?: ContentGenerator;
}
export interface PendingConfirmation {
@@ -162,12 +155,11 @@ export interface PendingConfirmation {
}
export class AppRig {
private renderResult: RenderInstance | undefined;
private renderResult: ReturnType<typeof renderWithProviders> | undefined;
private config: Config | undefined;
private settings: LoadedSettings | undefined;
private testDir: string;
private sessionId: string;
private appRigId: string;
private pendingConfirmations = new Map<string, PendingConfirmation>();
private breakpointTools = new Set<string | undefined>();
@@ -177,14 +169,12 @@ export class AppRig {
* True if a message was just sent but React hasn't yet reported a non-idle state.
*/
awaitingResponse = false;
activeStreamCount = 0;
constructor(private options: AppRigOptions = {}) {
const uniqueId = randomUUID();
this.testDir = fs.mkdtempSync(
path.join(os.tmpdir(), `gemini-app-rig-${uniqueId.slice(0, 8)}-`),
);
this.appRigId = path.basename(this.testDir).toLowerCase();
this.sessionId = `test-session-${uniqueId}`;
activeRigs.set(this.sessionId, this);
}
@@ -207,7 +197,7 @@ export class AppRig {
cwd: this.testDir,
debugMode: false,
model: 'test-model',
contentGenerator: this.options.contentGenerator,
fakeResponses: this.options.fakeResponsesPath,
interactive: true,
approvalMode,
policyEngineConfig,
@@ -219,44 +209,8 @@ export class AppRig {
};
this.config = makeFakeConfig(configParams);
// Track active streams directly from the client to prevent false idleness during synchronous mock yields
const client = this.config.getGeminiClient();
const originalStream = client.sendMessageStream.bind(client);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
client.sendMessageStream = async function* (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): AsyncGenerator<any, any, any> {
self.awaitingResponse = false;
self.activeStreamCount++;
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
yield* (originalStream as any)(...args);
} finally {
self.activeStreamCount = Math.max(0, self.activeStreamCount - 1);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
if (this.config.fakeResponses || this.options.contentGenerator) {
if (!this.options.contentGenerator && !this.config.fakeResponses) {
this.stubRefreshAuth();
}
if (!process.env['GEMINI_API_KEY']) {
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
}
MockShellExecutionService.setPassthrough(false);
} else {
if (!process.env['GEMINI_API_KEY']) {
throw new Error(
'GEMINI_API_KEY must be set in the environment for live model tests.',
);
}
// For live tests, we allow falling through to the real shell service if no mock matches
MockShellExecutionService.setPassthrough(true);
if (this.options.fakeResponsesPath) {
this.stubRefreshAuth();
}
this.setupMessageBusListeners();
@@ -272,6 +226,18 @@ export class AppRig {
private setupEnvironment() {
// Stub environment variables to avoid interference from developer's machine
vi.stubEnv('GEMINI_CLI_HOME', this.testDir);
if (this.options.fakeResponsesPath) {
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
MockShellExecutionService.setPassthrough(false);
} else {
if (!process.env['GEMINI_API_KEY']) {
throw new Error(
'GEMINI_API_KEY must be set in the environment for live model tests.',
);
}
// For live tests, we allow falling through to the real shell service if no mock matches
MockShellExecutionService.setPassthrough(true);
}
vi.stubEnv('GEMINI_DEFAULT_AUTH_TYPE', AuthType.USE_GEMINI);
}
@@ -389,23 +355,18 @@ export class AppRig {
* Returns true if the agent is currently busy (responding or executing tools).
*/
isBusy(): boolean {
const reactState = sessionStateMap.get(this.sessionId);
if (reactState && reactState !== StreamingState.Idle) {
this.awaitingResponse = false;
}
if (this.awaitingResponse || this.activeStreamCount > 0) {
if (this.awaitingResponse) {
return true;
}
const reactState = sessionStateMap.get(this.sessionId);
// If we have a React-based state, use it as the definitive signal.
// 'responding' and 'waiting-for-confirmation' both count as busy for the overall task.
if (reactState !== undefined) {
return reactState !== StreamingState.Idle;
}
// Fallback to tool tracking
// Fallback to tool tracking if React hasn't reported yet
const isAnyToolActive = this.toolCalls.some((tc) => {
if (
tc.status === CoreToolCallStatus.Executing ||
@@ -432,12 +393,12 @@ export class AppRig {
return isAnyToolActive || isAwaitingConfirmation;
}
async render() {
render() {
if (!this.config || !this.settings)
throw new Error('AppRig not initialized');
await act(async () => {
this.renderResult = await renderWithProviders(
act(() => {
this.renderResult = renderWithProviders(
<AppContainer
config={this.config!}
version="test-version"
@@ -696,21 +657,6 @@ export class AppRig {
}
}
/**
* Acts as an automated user ('Mock User') to prime the system with a specific
* history state before handing off control to a live trial or eval.
*
* @param prompts An array of user messages to send sequentially.
* @param timeout Optional timeout per interaction.
*/
async driveMockUser(prompts: string[], timeout = 60000) {
for (let i = 0; i < prompts.length; i++) {
const prompt = prompts[i];
await this.sendMessage(prompt);
await this.drainBreakpointsUntilIdle(undefined, timeout);
}
}
getConfig(): Config {
if (!this.config) throw new Error('AppRig not initialized');
return this.config;
@@ -808,10 +754,6 @@ export class AppRig {
// Forcefully clear IdeClient singleton promise
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(IdeClient as any).instancePromise = null;
// Reset Conseca singleton to avoid leaking config/state across tests
ConsecaSafetyChecker.resetInstance();
vi.clearAllMocks();
this.config = undefined;
@@ -828,79 +770,4 @@ export class AppRig {
}
}
}
getSentRequests() {
if (!this.config) throw new Error('AppRig not initialized');
return this.config.getContentGenerator().getSentRequests?.() || [];
}
/**
* Helper to get the curated history (contents) sent in the most recent model request.
* This method scrubs unstable data like temp paths and IDs for deterministic goldens.
*/
getLastSentRequestContents() {
const requests = this.getSentRequests();
if (requests.length === 0) return [];
const contents = requests[requests.length - 1].contents || [];
return this.scrubUnstableData(contents);
}
/**
* Gets the final curated history of the active chat session.
*/
getCuratedHistory() {
if (!this.config) throw new Error('AppRig not initialized');
const history = this.config.getGeminiClient().getChat().getHistory(true);
return this.scrubUnstableData(history);
}
private scrubUnstableData<
T extends
| Content[]
| GenerateContentParameters['contents']
| readonly Content[],
>(contents: T): T {
// Deeply scrub unstable data
const scrubbedString = JSON.stringify(contents)
.replace(new RegExp(this.testDir, 'g'), '<TEST_DIR>')
.replace(new RegExp(this.appRigId, 'g'), '<APP_RIG_ID>')
.replace(new RegExp(this.sessionId, 'g'), '<SESSION_ID>')
.replace(
/([a-zA-Z0-9_]+)_([0-9]{13})_([0-9]+)\.txt/g,
'$1_<TIMESTAMP>_<INDEX>.txt',
)
.replace(/Process Group PGID: \d+/g, 'Process Group PGID: <PGID>');
const scrubbed = JSON.parse(scrubbedString) as T;
if (Array.isArray(scrubbed) && scrubbed.length > 0) {
const firstItem = scrubbed[0] as Content;
if (firstItem.parts?.[0]?.text?.includes('<session_context>')) {
firstItem.parts[0].text = '<SESSION_CONTEXT>';
}
for (const content of scrubbed as Content[]) {
if (content.parts) {
for (const part of content.parts) {
if (part.functionCall) {
part.functionCall.id = '<CALL_ID>';
}
if (part.functionResponse) {
part.functionResponse.id = '<CALL_ID>';
if (
part.functionResponse.response !== null &&
typeof part.functionResponse.response === 'object' &&
'original_output_file' in part.functionResponse.response
) {
part.functionResponse.response['original_output_file'] =
'<TMP_FILE>';
}
}
}
}
}
}
return scrubbed;
}
}
@@ -1,3 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I will now fetch the data."},{"functionCall":{"name":"run_shell_command","args":{"command":"cat large.txt"}}}]},"finishReason":"STOP"}]}]}
{"method":"generateContent","response":{"candidates":[{"content":{"role":"model","parts":[{"text":"- Line 1: Header\n- Lines 2-5000: User data\n- Line 5001: Footer"}]},"finishReason":"STOP"}]}}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I got the summarized output. Task complete."}]},"finishReason":"STOP"}]}]}
@@ -46,19 +46,15 @@ describe('createMockCommandContext', () => {
const overrides = {
services: {
agentContext: { config: mockConfig },
config: mockConfig,
},
};
const context = createMockCommandContext(overrides);
expect(context.services.agentContext).toBeDefined();
expect(context.services.agentContext?.config?.getModel()).toBe(
'gemini-pro',
);
expect(context.services.agentContext?.config?.getProjectRoot()).toBe(
'/test/project',
);
expect(context.services.config).toBeDefined();
expect(context.services.config?.getModel()).toBe('gemini-pro');
expect(context.services.config?.getProjectRoot()).toBe('/test/project');
// Verify a default property on the same nested object is still there
expect(context.services.logger).toBeDefined();
@@ -36,7 +36,7 @@ export const createMockCommandContext = (
args: '',
},
services: {
agentContext: null,
config: null,
settings: {
merged: defaultMergedSettings,
+68 -33
View File
@@ -16,6 +16,8 @@ import { vi } from 'vitest';
import stripAnsi from 'strip-ansi';
import type React from 'react';
import { act, useState } from 'react';
import os from 'node:os';
import path from 'node:path';
import type { LoadedSettings } from '../config/settings.js';
import { KeypressProvider } from '../ui/contexts/KeypressContext.js';
import { SettingsContext } from '../ui/contexts/SettingsContext.js';
@@ -42,7 +44,7 @@ import {
type OverflowState,
} from '../ui/contexts/OverflowContext.js';
import { type Config } from '@google/gemini-cli-core';
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
import { AppContext, type AppState } from '../ui/contexts/AppContext.js';
import { createMockSettings } from './settings.js';
@@ -51,7 +53,6 @@ import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
import { pickDefaultThemeName } from '../ui/themes/theme.js';
import { generateSvgForTerminal } from './svg.js';
import { loadCliConfig, type CliArgs } from '../config/config.js';
export const persistentStateMock = new FakePersistentState();
@@ -65,9 +66,7 @@ if (process.env['NODE_ENV'] === 'test') {
}
vi.mock('../utils/persistentState.js', () => ({
get persistentState() {
return persistentStateMock;
},
persistentState: persistentStateMock,
}));
vi.mock('../ui/utils/terminalUtils.js', () => ({
@@ -487,6 +486,50 @@ export const simulateClick = async (
});
};
let mockConfigInternal: Config | undefined;
const getMockConfigInternal = (): Config => {
if (!mockConfigInternal) {
mockConfigInternal = makeFakeConfig({
targetDir: os.tmpdir(),
enableEventDrivenScheduler: true,
});
}
return mockConfigInternal;
};
const configProxy = new Proxy({} as Config, {
get(_target, prop) {
if (prop === 'getTargetDir') {
return () =>
path.join(
path.parse(process.cwd()).root,
'Users',
'test',
'project',
'foo',
'bar',
'and',
'some',
'more',
'directories',
'to',
'make',
'it',
'long',
);
}
if (prop === 'getUseBackgroundColor') {
return () => true;
}
const internal = getMockConfigInternal();
if (prop in internal) {
return internal[prop as keyof typeof internal];
}
throw new Error(`mockConfig does not have property ${String(prop)}`);
},
});
export const mockSettings = createMockSettings();
// A minimal mock UIState to satisfy the context provider.
@@ -596,7 +639,7 @@ const ContextCapture: React.FC<{ children: React.ReactNode }> = ({
return <>{children}</>;
};
export const renderWithProviders = async (
export const renderWithProviders = (
component: React.ReactElement,
{
shellFocus = true,
@@ -604,7 +647,8 @@ export const renderWithProviders = async (
uiState: providedUiState,
width,
mouseEventsEnabled = false,
config,
config = configProxy as unknown as Config,
uiActions,
persistentState,
appState = mockAppState,
@@ -622,15 +666,13 @@ export const renderWithProviders = async (
};
appState?: AppState;
} = {},
): Promise<
RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
}
> => {
): RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
} => {
const baseState: UIState = new Proxy(
{ ...baseMockUiState, ...providedUiState },
{
@@ -659,15 +701,8 @@ export const renderWithProviders = async (
persistentStateMock.mockClear();
const terminalWidth = width ?? baseState.terminalWidth;
if (!config) {
config = await loadCliConfig(
settings.merged,
'random-session-id',
{} as unknown as CliArgs,
{ cwd: '/' },
);
}
const finalSettings = settings;
const finalConfig = config;
const mainAreaWidth = terminalWidth;
@@ -697,8 +732,8 @@ export const renderWithProviders = async (
const wrapWithProviders = (comp: React.ReactElement) => (
<AppContext.Provider value={appState}>
<ConfigContext.Provider value={config}>
<SettingsContext.Provider value={settings}>
<ConfigContext.Provider value={finalConfig}>
<SettingsContext.Provider value={finalSettings}>
<UIStateContext.Provider value={finalUiState}>
<VimModeProvider>
<ShellFocusContext.Provider value={shellFocus}>
@@ -709,7 +744,7 @@ export const renderWithProviders = async (
<UIActionsContext.Provider value={finalUIActions}>
<OverflowProvider>
<ToolActionsProvider
config={config}
config={finalConfig}
toolCalls={allToolCalls}
>
<AskUserActionsProvider
@@ -828,7 +863,7 @@ export function renderHook<Result, Props>(
return { result, rerender, unmount, waitUntilReady, generateSvg };
}
export async function renderHookWithProviders<Result, Props>(
export function renderHookWithProviders<Result, Props>(
renderCallback: (props: Props) => Result,
options: {
initialProps?: Props;
@@ -841,13 +876,13 @@ export async function renderHookWithProviders<Result, Props>(
mouseEventsEnabled?: boolean;
config?: Config;
} = {},
): Promise<{
): {
result: { current: Result };
rerender: (props?: Props) => void;
unmount: () => void;
waitUntilReady: () => Promise<void>;
generateSvg: () => string;
}> {
} {
const result = { current: undefined as unknown as Result };
let setPropsFn: ((props: Props) => void) | undefined;
@@ -866,8 +901,8 @@ export async function renderHookWithProviders<Result, Props>(
let renderResult: ReturnType<typeof render>;
await act(async () => {
renderResult = await renderWithProviders(
act(() => {
renderResult = renderWithProviders(
<Wrapper>
{}
<TestComponent initialProps={options.initialProps as Props} />
+21 -11
View File
@@ -94,10 +94,11 @@ describe('App', () => {
};
it('should render main content and composer when not quitting', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: false }),
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
@@ -115,10 +116,11 @@ describe('App', () => {
quittingMessages: [{ id: 1, type: 'user', text: 'test' }],
} as UIState;
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: quittingUIState,
config: makeFakeConfig({ useAlternateBuffer: false }),
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
@@ -136,10 +138,11 @@ describe('App', () => {
pendingHistoryItems: [{ type: 'user', text: 'pending item' }],
} as UIState;
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: quittingUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -156,10 +159,11 @@ describe('App', () => {
dialogsVisible: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: dialogUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -183,10 +187,11 @@ describe('App', () => {
[stateKey]: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -200,10 +205,11 @@ describe('App', () => {
it('should render ScreenReaderAppLayout when screen reader is enabled', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -219,10 +225,11 @@ describe('App', () => {
it('should render DefaultAppLayout when screen reader is not enabled', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -274,7 +281,7 @@ describe('App', () => {
vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true);
vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: stateWithConfirmingTool,
@@ -295,10 +302,11 @@ describe('App', () => {
describe('Snapshots', () => {
it('renders default layout correctly', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -309,10 +317,11 @@ describe('App', () => {
it('renders screen reader layout correctly', async () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
@@ -326,10 +335,11 @@ describe('App', () => {
...mockUIState,
dialogsVisible: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: dialogUIState,
config: makeFakeConfig({ useAlternateBuffer: true }),
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
},
);
+4 -4
View File
@@ -212,7 +212,7 @@ import { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useErrorCount } from './hooks/useConsoleMessages.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useVim } from './hooks/vim.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
@@ -294,7 +294,7 @@ describe('AppContainer State Management', () => {
const mockedUseSettingsCommand = useSettingsCommand as Mock;
const mockedUseModelCommand = useModelCommand as Mock;
const mockedUseSlashCommandProcessor = useSlashCommandProcessor as Mock;
const mockedUseConsoleMessages = useErrorCount as Mock;
const mockedUseConsoleMessages = useConsoleMessages as Mock;
const mockedUseGeminiStream = useGeminiStream as Mock;
const mockedUseVim = useVim as Mock;
const mockedUseFolderTrust = useFolderTrust as Mock;
@@ -396,9 +396,9 @@ describe('AppContainer State Management', () => {
confirmationRequest: null,
});
mockedUseConsoleMessages.mockReturnValue({
errorCount: 0,
consoleMessages: [],
handleNewMessage: vi.fn(),
clearErrorCount: vi.fn(),
clearConsoleMessages: vi.fn(),
});
mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
mockedUseVim.mockReturnValue({ handleInput: vi.fn() });
+26 -15
View File
@@ -103,7 +103,7 @@ import {
useOverflowActions,
useOverflowState,
} from './contexts/OverflowContext.js';
import { useErrorCount } from './hooks/useConsoleMessages.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { calculatePromptWidths } from './components/InputPrompt.js';
import { calculateMainAreaWidth } from './utils/ui-sizing.js';
@@ -552,7 +552,8 @@ export const AppContainer = (props: AppContainerProps) => {
};
}, [settings]);
const { errorCount, clearErrorCount } = useErrorCount();
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
useConsoleMessages();
const mainAreaWidth = calculateMainAreaWidth(terminalWidth, config);
// Derive widths for InputPrompt using shared helper
@@ -1007,18 +1008,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
Date.now(),
);
try {
let flattenedMemory: string;
let fileCount: number;
const { memoryContent, fileCount } =
await refreshServerHierarchicalMemory(config);
if (config.isJitContextEnabled()) {
await config.getContextManager()?.refresh();
flattenedMemory = flattenMemory(config.getUserMemory());
fileCount = config.getGeminiMdFileCount();
} else {
const result = await refreshServerHierarchicalMemory(config);
flattenedMemory = flattenMemory(result.memoryContent);
fileCount = result.fileCount;
}
const flattenedMemory = flattenMemory(memoryContent);
historyManager.addItem(
{
@@ -1379,11 +1372,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
// Explicitly hide the expansion hint and clear its x-second timer when clearing the screen.
triggerExpandHint(null);
historyManager.clearItems();
clearErrorCount();
clearConsoleMessagesState();
refreshStatic();
}, [
historyManager,
clearErrorCount,
clearConsoleMessagesState,
refreshStatic,
reset,
triggerExpandHint,
@@ -1990,6 +1983,22 @@ Logging in with Google... Restarting Gemini CLI to continue.
};
}, [historyManager]);
const filteredConsoleMessages = useMemo(() => {
if (config.getDebugMode()) {
return consoleMessages;
}
return consoleMessages.filter((msg) => msg.type !== 'debug');
}, [consoleMessages, config]);
// Computed values
const errorCount = useMemo(
() =>
filteredConsoleMessages
.filter((msg) => msg.type === 'error')
.reduce((total, msg) => total + msg.count, 0),
[filteredConsoleMessages],
);
const nightly = props.version.includes('nightly');
const dialogsVisible =
@@ -2224,6 +2233,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
constrainHeight,
showErrorDetails,
showFullTodos,
filteredConsoleMessages,
ideContextState,
renderMarkdown,
ctrlCPressedOnce: ctrlCPressCount >= 1,
@@ -2351,6 +2361,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
constrainHeight,
showErrorDetails,
showFullTodos,
filteredConsoleMessages,
ideContextState,
renderMarkdown,
ctrlCPressCount,
@@ -53,7 +53,7 @@ describe('IdeIntegrationNudge', () => {
});
it('renders correctly with default options', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} />,
);
await waitUntilReady();
@@ -68,7 +68,7 @@ describe('IdeIntegrationNudge', () => {
it('handles "Yes" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
@@ -89,7 +89,7 @@ describe('IdeIntegrationNudge', () => {
it('handles "No" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
@@ -115,7 +115,7 @@ describe('IdeIntegrationNudge', () => {
it('handles "Dismiss" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
@@ -146,7 +146,7 @@ describe('IdeIntegrationNudge', () => {
it('handles Escape key press', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
const { stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
@@ -173,10 +173,9 @@ describe('IdeIntegrationNudge', () => {
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '/tmp');
const onComplete = vi.fn();
const { lastFrame, stdin, waitUntilReady, unmount } =
await renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
const { lastFrame, stdin, waitUntilReady, unmount } = renderWithProviders(
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
);
await waitUntilReady();
+17 -17
View File
@@ -143,7 +143,7 @@ describe('AuthDialog', () => {
for (const [key, value] of Object.entries(env)) {
vi.stubEnv(key, value as string);
}
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -161,7 +161,7 @@ describe('AuthDialog', () => {
it('filters auth types when enforcedType is set', async () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -173,7 +173,7 @@ describe('AuthDialog', () => {
it('sets initial index to 0 when enforcedType is set', async () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -213,7 +213,7 @@ describe('AuthDialog', () => {
},
])('selects initial auth type $desc', async ({ setup, expected }) => {
setup();
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -226,7 +226,7 @@ describe('AuthDialog', () => {
describe('handleAuthSelect', () => {
it('calls onAuthError if validation fails', async () => {
mockedValidateAuthMethod.mockReturnValue('Invalid method');
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -245,7 +245,7 @@ describe('AuthDialog', () => {
it('sets auth context with requiresRestart: true for LOGIN_WITH_GOOGLE', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -261,7 +261,7 @@ describe('AuthDialog', () => {
it('sets auth context with empty object for other auth types', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -278,7 +278,7 @@ describe('AuthDialog', () => {
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -297,7 +297,7 @@ describe('AuthDialog', () => {
vi.stubEnv('GEMINI_API_KEY', ''); // Empty string
// props.settings.merged.security.auth.selectedType is undefined here
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -316,7 +316,7 @@ describe('AuthDialog', () => {
// process.env['GEMINI_API_KEY'] is not set
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -337,7 +337,7 @@ describe('AuthDialog', () => {
props.settings.merged.security.auth.selectedType =
AuthType.LOGIN_WITH_GOOGLE;
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -360,7 +360,7 @@ describe('AuthDialog', () => {
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -383,7 +383,7 @@ describe('AuthDialog', () => {
it('displays authError when provided', async () => {
props.authError = 'Something went wrong';
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -429,7 +429,7 @@ describe('AuthDialog', () => {
},
])('$desc', async ({ setup, expectations }) => {
setup();
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -442,7 +442,7 @@ describe('AuthDialog', () => {
describe('Snapshots', () => {
it('renders correctly with default props', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -452,7 +452,7 @@ describe('AuthDialog', () => {
it('renders correctly with auth error', async () => {
props.authError = 'Something went wrong';
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -462,7 +462,7 @@ describe('AuthDialog', () => {
it('renders correctly with enforced auth type', async () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
@@ -73,7 +73,7 @@ describe('BannedAccountDialog', () => {
});
it('renders the suspension message from accountSuspensionInfo', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -89,7 +89,7 @@ describe('BannedAccountDialog', () => {
});
it('renders menu options with appeal link text from response', async () => {
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -109,7 +109,7 @@ describe('BannedAccountDialog', () => {
const infoWithoutUrl: AccountSuspensionInfo = {
message: 'Account suspended.',
};
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={infoWithoutUrl}
onExit={onExit}
@@ -129,7 +129,7 @@ describe('BannedAccountDialog', () => {
message: 'Account suspended.',
appealUrl: 'https://example.com/appeal',
};
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={infoWithoutLinkText}
onExit={onExit}
@@ -143,7 +143,7 @@ describe('BannedAccountDialog', () => {
});
it('opens browser when appeal option is selected', async () => {
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -162,7 +162,7 @@ describe('BannedAccountDialog', () => {
it('shows URL when browser cannot be launched', async () => {
mockedShouldLaunchBrowser.mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -180,7 +180,7 @@ describe('BannedAccountDialog', () => {
});
it('calls onExit when "Exit" is selected', async () => {
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -196,7 +196,7 @@ describe('BannedAccountDialog', () => {
});
it('calls onChangeAuth when "Change authentication" is selected', async () => {
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -212,7 +212,7 @@ describe('BannedAccountDialog', () => {
});
it('exits on escape key', async () => {
const { waitUntilReady, unmount } = await renderWithProviders(
const { waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -227,7 +227,7 @@ describe('BannedAccountDialog', () => {
});
it('renders snapshot correctly', async () => {
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<BannedAccountDialog
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
onExit={onExit}
@@ -36,12 +36,10 @@ describe('aboutCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true),
getUserTierName: vi.fn().mockReturnValue(undefined),
},
config: {
getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true),
getUserTierName: vi.fn().mockReturnValue(undefined),
},
settings: {
merged: {
@@ -59,10 +57,9 @@ describe('aboutCommand', () => {
} as unknown as CommandContext);
vi.mocked(getVersion).mockResolvedValue('test-version');
vi.spyOn(
mockContext.services.agentContext!.config,
'getModel',
).mockReturnValue('test-model');
vi.spyOn(mockContext.services.config!, 'getModel').mockReturnValue(
'test-model',
);
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-gcp-project';
Object.defineProperty(process, 'platform', {
value: 'test-os',
@@ -163,9 +160,9 @@ describe('aboutCommand', () => {
});
it('should display the tier when getUserTierName returns a value', async () => {
vi.mocked(
mockContext.services.agentContext!.config.getUserTierName,
).mockReturnValue('Enterprise Tier');
vi.mocked(mockContext.services.config!.getUserTierName).mockReturnValue(
'Enterprise Tier',
);
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
+3 -4
View File
@@ -34,8 +34,7 @@ export const aboutCommand: SlashCommand = {
process.env['SEATBELT_PROFILE'] || 'unknown'
})`;
}
const modelVersion =
context.services.agentContext?.config.getModel() || 'Unknown';
const modelVersion = context.services.config?.getModel() || 'Unknown';
const cliVersion = await getVersion();
const selectedAuthType =
context.services.settings.merged.security.auth.selectedType || '';
@@ -49,7 +48,7 @@ export const aboutCommand: SlashCommand = {
});
const userEmail = cachedAccount ?? undefined;
const tier = context.services.agentContext?.config.getUserTierName();
const tier = context.services.config?.getUserTierName();
const aboutItem: Omit<HistoryItemAbout, 'id'> = {
type: MessageType.ABOUT,
@@ -69,7 +68,7 @@ export const aboutCommand: SlashCommand = {
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.agentContext?.config.getIdeMode()) {
if (!context.services.config?.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
@@ -26,7 +26,6 @@ describe('agentsCommand', () => {
let mockContext: ReturnType<typeof createMockCommandContext>;
let mockConfig: {
getAgentRegistry: ReturnType<typeof vi.fn>;
config: Config;
};
beforeEach(() => {
@@ -38,14 +37,11 @@ describe('agentsCommand', () => {
getAllAgentNames: vi.fn().mockReturnValue([]),
reload: vi.fn(),
}),
get config() {
return this as unknown as Config;
},
};
mockContext = createMockCommandContext({
services: {
agentContext: mockConfig as unknown as Config,
config: mockConfig as unknown as Config,
settings: {
workspace: { path: '/mock/path' },
merged: { agents: { overrides: {} } },
@@ -57,7 +53,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
@@ -230,7 +226,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available for enable', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { agentContext: null },
services: { config: null },
});
const enableCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'enable',
@@ -336,7 +332,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available for disable', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { agentContext: null },
services: { config: null },
});
const disableCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'disable',
@@ -437,7 +433,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { agentContext: null },
services: { config: null },
});
const configCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'config',
+8 -11
View File
@@ -21,7 +21,7 @@ const agentsListCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
const config = context.services.agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -61,8 +61,7 @@ async function enableAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const config = context.services.agentContext?.config;
const { settings } = context.services;
const { config, settings } = context.services;
if (!config) {
return {
type: 'message',
@@ -138,8 +137,7 @@ async function disableAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const config = context.services.agentContext?.config;
const { settings } = context.services;
const { config, settings } = context.services;
if (!config) {
return {
type: 'message',
@@ -218,7 +216,7 @@ async function configAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const config = context.services.agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -268,8 +266,7 @@ async function configAction(
}
function completeAgentsToEnable(context: CommandContext, partialArg: string) {
const config = context.services.agentContext?.config;
const { settings } = context.services;
const { config, settings } = context.services;
if (!config) return [];
const overrides = settings.merged.agents.overrides;
@@ -281,7 +278,7 @@ function completeAgentsToEnable(context: CommandContext, partialArg: string) {
}
function completeAgentsToDisable(context: CommandContext, partialArg: string) {
const config = context.services.agentContext?.config;
const { config } = context.services;
if (!config) return [];
const agentRegistry = config.getAgentRegistry();
@@ -290,7 +287,7 @@ function completeAgentsToDisable(context: CommandContext, partialArg: string) {
}
function completeAllAgents(context: CommandContext, partialArg: string) {
const config = context.services.agentContext?.config;
const { config } = context.services;
if (!config) return [];
const agentRegistry = config.getAgentRegistry();
@@ -331,7 +328,7 @@ const agentsReloadCommand: SlashCommand = {
description: 'Reload the agent registry',
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => {
const config = context.services.agentContext?.config;
const { config } = context.services;
const agentRegistry = config?.getAgentRegistry();
if (!agentRegistry) {
return {
@@ -9,7 +9,6 @@ import { authCommand } from './authCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { SettingScope } from '../../config/settings.js';
import type { GeminiClient } from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual('@google/gemini-cli-core');
@@ -25,10 +24,8 @@ describe('authCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
agentContext: {
geminiClient: {
stripThoughtsFromHistory: vi.fn(),
},
config: {
getGeminiClient: vi.fn(),
},
},
});
@@ -104,19 +101,17 @@ describe('authCommand', () => {
const mockStripThoughts = vi.fn();
const mockClient = {
stripThoughtsFromHistory: mockStripThoughts,
} as unknown as GeminiClient;
if (mockContext.services.agentContext?.config) {
mockContext.services.agentContext.config.getGeminiClient = vi.fn(
() => mockClient,
);
} as unknown as ReturnType<
NonNullable<typeof mockContext.services.config>['getGeminiClient']
>;
if (mockContext.services.config) {
mockContext.services.config.getGeminiClient = vi.fn(() => mockClient);
}
await logoutCommand!.action!(mockContext, '');
expect(
mockContext.services.agentContext?.geminiClient
.stripThoughtsFromHistory,
).toHaveBeenCalled();
expect(mockStripThoughts).toHaveBeenCalled();
});
it('should return logout action to signal explicit state change', async () => {
@@ -128,7 +123,7 @@ describe('authCommand', () => {
it('should handle missing config gracefully', async () => {
const logoutCommand = authCommand.subCommands?.[1];
mockContext.services.agentContext = null;
mockContext.services.config = null;
const result = await logoutCommand!.action!(mockContext, '');
+1 -1
View File
@@ -39,7 +39,7 @@ const authLogoutCommand: SlashCommand = {
undefined,
);
// Strip thoughts from history instead of clearing completely
context.services.agentContext?.geminiClient.stripThoughtsFromHistory();
context.services.config?.getGeminiClient()?.stripThoughtsFromHistory();
// Return logout action to signal explicit state change
return {
type: 'logout',
+23 -29
View File
@@ -83,18 +83,16 @@ describe('bugCommand', () => {
it('should generate the default GitHub issue URL', async () => {
const mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
},
geminiClient: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getGeminiClient: () => ({
getChat: () => ({
getHistory: () => [],
}),
},
}),
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
},
},
});
@@ -128,20 +126,18 @@ describe('bugCommand', () => {
];
const mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
storage: {
getProjectTempDir: () => '/tmp/gemini',
},
},
geminiClient: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getGeminiClient: () => ({
getChat: () => ({
getHistory: () => history,
}),
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
storage: {
getProjectTempDir: () => '/tmp/gemini',
},
},
},
@@ -176,18 +172,16 @@ describe('bugCommand', () => {
'https://internal.bug-tracker.com/new?desc={title}&details={info}';
const mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
},
geminiClient: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getGeminiClient: () => ({
getChat: () => ({
getHistory: () => [],
}),
},
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
},
},
});
+4 -4
View File
@@ -32,8 +32,8 @@ export const bugCommand: SlashCommand = {
autoExecute: false,
action: async (context: CommandContext, args?: string): Promise<void> => {
const bugDescription = (args || '').trim();
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
const osVersion = `${process.platform} ${process.version}`;
let sandboxEnv = 'no sandbox';
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
@@ -73,7 +73,7 @@ export const bugCommand: SlashCommand = {
info += `* **IDE Client:** ${ideClient}\n`;
}
const chat = agentContext?.geminiClient?.getChat();
const chat = config?.getGeminiClient()?.getChat();
const history = chat?.getHistory() || [];
let historyFileMessage = '';
let problemValue = bugDescription;
@@ -134,7 +134,7 @@ export const bugCommand: SlashCommand = {
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.agentContext?.config.getIdeMode()) {
if (!context.services.config?.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
@@ -70,19 +70,18 @@ describe('chatCommand', () => {
mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getProjectRoot: () => '/project/root',
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
storage: {
getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash',
},
config: {
getProjectRoot: () => '/project/root',
getGeminiClient: () =>
({
getChat: mockGetChat,
}) as unknown as GeminiClient,
storage: {
getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash',
},
geminiClient: {
getChat: mockGetChat,
} as unknown as GeminiClient,
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
},
logger: {
saveCheckpoint: mockSaveCheckpoint,
@@ -699,11 +698,7 @@ Hi there!`;
beforeEach(() => {
mockGetLatestApiRequest = vi.fn();
if (!mockContext.services.agentContext!.config) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockContext.services.agentContext!.config as any) = {};
}
mockContext.services.agentContext!.config.getLatestApiRequest =
mockContext.services.config!.getLatestApiRequest =
mockGetLatestApiRequest;
vi.spyOn(process, 'cwd').mockReturnValue('/project/root');
vi.spyOn(Date, 'now').mockReturnValue(1234567890);
+6 -8
View File
@@ -35,7 +35,7 @@ const getSavedChatTags = async (
context: CommandContext,
mtSortDesc: boolean,
): Promise<ChatDetail[]> => {
const cfg = context.services.agentContext?.config;
const cfg = context.services.config;
const geminiDir = cfg?.storage?.getProjectTempDir();
if (!geminiDir) {
return [];
@@ -103,8 +103,7 @@ const saveCommand: SlashCommand = {
};
}
const { logger } = context.services;
const config = context.services.agentContext?.config;
const { logger, config } = context.services;
await logger.initialize();
if (!context.overwriteConfirmed) {
@@ -126,7 +125,7 @@ const saveCommand: SlashCommand = {
}
}
const chat = context.services.agentContext?.geminiClient?.getChat();
const chat = config?.getGeminiClient()?.getChat();
if (!chat) {
return {
type: 'message',
@@ -173,8 +172,7 @@ const resumeCheckpointCommand: SlashCommand = {
};
}
const { logger } = context.services;
const config = context.services.agentContext?.config;
const { logger, config } = context.services;
await logger.initialize();
const checkpoint = await logger.loadCheckpoint(tag);
const conversation = checkpoint.history;
@@ -300,7 +298,7 @@ const shareCommand: SlashCommand = {
};
}
const chat = context.services.agentContext?.geminiClient?.getChat();
const chat = context.services.config?.getGeminiClient()?.getChat();
if (!chat) {
return {
type: 'message',
@@ -346,7 +344,7 @@ export const debugCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context): Promise<MessageActionReturn> => {
const req = context.services.agentContext?.config.getLatestApiRequest();
const req = context.services.config?.getLatestApiRequest();
if (!req) {
return {
type: 'message',
@@ -36,25 +36,24 @@ describe('clearCommand', () => {
mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getEnableHooks: vi.fn().mockReturnValue(false),
setSessionId: vi.fn(),
getMessageBus: vi.fn().mockReturnValue(undefined),
getHookSystem: vi.fn().mockReturnValue({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
}),
injectionService: {
clear: mockHintClear,
},
config: {
getGeminiClient: () =>
({
resetChat: mockResetChat,
getChat: () => ({
getChatRecordingService: mockGetChatRecordingService,
}),
}) as unknown as GeminiClient,
setSessionId: vi.fn(),
getEnableHooks: vi.fn().mockReturnValue(false),
getMessageBus: vi.fn().mockReturnValue(undefined),
getHookSystem: vi.fn().mockReturnValue({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
}),
injectionService: {
clear: mockHintClear,
},
geminiClient: {
resetChat: mockResetChat,
getChat: () => ({
getChatRecordingService: mockGetChatRecordingService,
}),
} as unknown as GeminiClient,
},
},
});
@@ -99,7 +98,7 @@ describe('clearCommand', () => {
const nullConfigContext = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
+2 -2
View File
@@ -20,8 +20,8 @@ export const clearCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, _args) => {
const geminiClient = context.services.agentContext?.geminiClient;
const config = context.services.agentContext?.config;
const geminiClient = context.services.config?.getGeminiClient();
const config = context.services.config;
// Fire SessionEnd hook before clearing
const hookSystem = config?.getHookSystem();
@@ -22,10 +22,11 @@ describe('compressCommand', () => {
mockTryCompressChat = vi.fn();
context = createMockCommandContext({
services: {
agentContext: {
geminiClient: {
tryCompressChat: mockTryCompressChat,
} as unknown as GeminiClient,
config: {
getGeminiClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
},
},
});
@@ -39,11 +39,9 @@ export const compressCommand: SlashCommand = {
try {
ui.setPendingItem(pendingMessage);
const promptId = `compress-${Date.now()}`;
const compressed =
await context.services.agentContext?.geminiClient?.tryCompressChat(
promptId,
true,
);
const compressed = await context.services.config
?.getGeminiClient()
?.tryCompressChat(promptId, true);
if (compressed) {
ui.addItem(
{
@@ -29,10 +29,10 @@ describe('copyCommand', () => {
mockContext = createMockCommandContext({
services: {
agentContext: {
geminiClient: {
config: {
getGeminiClient: () => ({
getChat: mockGetChat,
},
}),
},
},
});
@@ -301,7 +301,7 @@ describe('copyCommand', () => {
if (!copyCommand.action) throw new Error('Command has no action');
const nullConfigContext = createMockCommandContext({
services: { agentContext: null },
services: { config: null },
});
const result = await copyCommand.action(nullConfigContext, '');
+1 -1
View File
@@ -18,7 +18,7 @@ export const copyCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, _args): Promise<SlashCommandActionReturn | void> => {
const chat = context.services.agentContext?.geminiClient?.getChat();
const chat = context.services.config?.getGeminiClient()?.getChat();
const history = chat?.getHistory();
// Get the last message from the AI (model role)
@@ -85,14 +85,11 @@ describe('directoryCommand', () => {
getFileFilteringOptions: () => ({ ignore: [], include: [] }),
setUserMemory: vi.fn(),
setGeminiMdFileCount: vi.fn(),
get config() {
return this;
},
} as unknown as Config;
mockContext = {
services: {
agentContext: mockConfig,
config: mockConfig,
settings: {
merged: {
memoryDiscoveryMaxDirs: 1000,
@@ -60,7 +60,7 @@ async function finishAddingDirectories(
}
if (added.length > 0) {
const gemini = config.geminiClient;
const gemini = config.getGeminiClient();
if (gemini) {
await gemini.addDirectoryContext();
@@ -110,9 +110,9 @@ export const directoryCommand: SlashCommand = {
// Filter out existing directories
let filteredSuggestions = suggestions;
if (context.services.agentContext?.config) {
if (context.services.config) {
const workspaceContext =
context.services.agentContext.config.getWorkspaceContext();
context.services.config.getWorkspaceContext();
const existingDirs = new Set(
workspaceContext.getDirectories().map((dir) => path.resolve(dir)),
);
@@ -144,11 +144,11 @@ export const directoryCommand: SlashCommand = {
action: async (context: CommandContext, args: string) => {
const {
ui: { addItem },
services: { agentContext, settings },
services: { config, settings },
} = context;
const [...rest] = args.split(' ');
if (!agentContext) {
if (!config) {
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
@@ -156,7 +156,7 @@ export const directoryCommand: SlashCommand = {
return;
}
if (agentContext.config.isRestrictiveSandbox()) {
if (config.isRestrictiveSandbox()) {
return {
type: 'message' as const,
messageType: 'error' as const,
@@ -181,7 +181,7 @@ export const directoryCommand: SlashCommand = {
const errors: string[] = [];
const alreadyAdded: string[] = [];
const workspaceContext = agentContext.config.getWorkspaceContext();
const workspaceContext = config.getWorkspaceContext();
const currentWorkspaceDirs = workspaceContext.getDirectories();
const pathsToProcess: string[] = [];
@@ -252,7 +252,7 @@ export const directoryCommand: SlashCommand = {
trustedDirs={added}
errors={errors}
finishAddingDirectories={finishAddingDirectories}
config={agentContext.config}
config={config}
addItem={addItem}
/>
),
@@ -264,12 +264,7 @@ export const directoryCommand: SlashCommand = {
errors.push(...result.errors);
}
await finishAddingDirectories(
agentContext.config,
addItem,
added,
errors,
);
await finishAddingDirectories(config, addItem, added, errors);
return;
},
},
@@ -280,16 +275,16 @@ export const directoryCommand: SlashCommand = {
action: async (context: CommandContext) => {
const {
ui: { addItem },
services: { agentContext },
services: { config },
} = context;
if (!agentContext) {
if (!config) {
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
});
return;
}
const workspaceContext = agentContext.config.getWorkspaceContext();
const workspaceContext = config.getWorkspaceContext();
const directories = workspaceContext.getDirectories();
const directoryList = directories.map((dir) => `- ${dir}`).join('\n');
addItem({
@@ -161,16 +161,14 @@ describe('extensionsCommand', () => {
mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getExtensions: mockGetExtensions,
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionLoader),
getWorkingDir: () => '/test/dir',
reloadSkills: mockReloadSkills,
getAgentRegistry: vi.fn().mockReturnValue({
reload: mockReloadAgents,
}),
},
config: {
getExtensions: mockGetExtensions,
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionLoader),
getWorkingDir: () => '/test/dir',
reloadSkills: mockReloadSkills,
getAgentRegistry: vi.fn().mockReturnValue({
reload: mockReloadAgents,
}),
},
},
ui: {
@@ -919,7 +917,7 @@ describe('extensionsCommand', () => {
expect(restartAction).not.toBeNull();
mockRestartExtension = vi.fn();
mockContext.services.agentContext!.config.getExtensionLoader = vi
mockContext.services.config!.getExtensionLoader = vi
.fn()
.mockImplementation(() => ({
getExtensions: mockGetExtensions,
@@ -929,7 +927,7 @@ describe('extensionsCommand', () => {
});
it('should show a message if no extensions are installed', async () => {
mockContext.services.agentContext!.config.getExtensionLoader = vi
mockContext.services.config!.getExtensionLoader = vi
.fn()
.mockImplementation(() => ({
getExtensions: () => [],
@@ -1019,7 +1017,7 @@ describe('extensionsCommand', () => {
});
it('shows an error if no extension loader is available', async () => {
mockContext.services.agentContext!.config.getExtensionLoader = vi.fn();
mockContext.services.config!.getExtensionLoader = vi.fn();
await restartAction!(mockContext, '--all');
@@ -54,8 +54,8 @@ function showMessageIfNoExtensions(
}
async function listAction(context: CommandContext) {
const extensions = context.services.agentContext?.config
? listExtensions(context.services.agentContext.config)
const extensions = context.services.config
? listExtensions(context.services.config)
: [];
if (showMessageIfNoExtensions(context, extensions)) {
@@ -88,8 +88,8 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
(resolve) => (resolveUpdateComplete = resolve),
);
const extensions = context.services.agentContext?.config
? listExtensions(context.services.agentContext.config)
const extensions = context.services.config
? listExtensions(context.services.config)
: [];
if (showMessageIfNoExtensions(context, extensions)) {
@@ -128,7 +128,7 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
},
});
if (names?.length) {
const extensions = listExtensions(context.services.agentContext!.config);
const extensions = listExtensions(context.services.config!);
for (const name of names) {
const extension = extensions.find(
(extension) => extension.name === name,
@@ -156,8 +156,7 @@ async function restartAction(
context: CommandContext,
args: string,
): Promise<void> {
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
const extensionLoader = context.services.config?.getExtensionLoader();
if (!extensionLoader) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -236,8 +235,8 @@ async function restartAction(
if (failures.length < extensionsToRestart.length) {
try {
await context.services.agentContext?.config.reloadSkills();
await context.services.agentContext?.config.getAgentRegistry()?.reload();
await context.services.config?.reloadSkills();
await context.services.config?.getAgentRegistry()?.reload();
} catch (error) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -275,8 +274,7 @@ async function exploreAction(
const useRegistryUI = settings.experimental?.extensionRegistry;
if (useRegistryUI) {
const extensionManager =
context.services.agentContext?.config.getExtensionLoader();
const extensionManager = context.services.config?.getExtensionLoader();
if (extensionManager instanceof ExtensionManager) {
return {
type: 'custom_dialog' as const,
@@ -333,8 +331,7 @@ function getEnableDisableContext(
names: string[];
scope: SettingScope;
} | null {
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
const extensionLoader = context.services.config?.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -434,8 +431,7 @@ async function enableAction(context: CommandContext, args: string) {
if (extension?.mcpServers) {
const mcpEnablementManager = McpServerEnablementManager.getInstance();
const mcpClientManager =
context.services.agentContext?.config.getMcpClientManager();
const mcpClientManager = context.services.config?.getMcpClientManager();
const enabledServers = await mcpEnablementManager.autoEnableServers(
Object.keys(extension.mcpServers ?? {}),
);
@@ -467,8 +463,7 @@ async function installAction(
args: string,
requestConsentOverride?: (consent: string) => Promise<boolean>,
) {
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
const extensionLoader = context.services.config?.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -534,8 +529,7 @@ async function installAction(
}
async function linkAction(context: CommandContext, args: string) {
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
const extensionLoader = context.services.config?.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -599,8 +593,7 @@ async function linkAction(context: CommandContext, args: string) {
}
async function uninstallAction(context: CommandContext, args: string) {
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
const extensionLoader = context.services.config?.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -699,8 +692,7 @@ async function configAction(context: CommandContext, args: string) {
}
}
const extensionManager =
context.services.agentContext?.config.getExtensionLoader();
const extensionManager = context.services.config?.getExtensionLoader();
if (!(extensionManager instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -737,7 +729,7 @@ export function completeExtensions(
context: CommandContext,
partialArg: string,
) {
let extensions = context.services.agentContext?.config.getExtensions() ?? [];
let extensions = context.services.config?.getExtensions() ?? [];
if (context.invocation?.name === 'enable') {
extensions = extensions.filter((ext) => !ext.isActive);
@@ -93,7 +93,7 @@ describe('hooksCommand', () => {
// Create mock context with config and settings
mockContext = createMockCommandContext({
services: {
agentContext: { config: mockConfig },
config: mockConfig,
settings: mockSettings,
},
});
@@ -141,7 +141,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
@@ -225,7 +225,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
@@ -338,7 +338,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
@@ -470,7 +470,7 @@ describe('hooksCommand', () => {
it('should return empty array when config is not available', () => {
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
@@ -567,7 +567,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
@@ -691,7 +691,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
+7 -14
View File
@@ -27,8 +27,7 @@ import { HooksDialog } from '../components/HooksDialog.js';
function panelAction(
context: CommandContext,
): MessageActionReturn | OpenCustomDialogActionReturn {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -56,8 +55,7 @@ async function enableAction(
context: CommandContext,
args: string,
): Promise<void | MessageActionReturn> {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -110,8 +108,7 @@ async function disableAction(
context: CommandContext,
args: string,
): Promise<void | MessageActionReturn> {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -166,8 +163,7 @@ function completeEnabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) return [];
const hookSystem = config.getHookSystem();
@@ -187,8 +183,7 @@ function completeDisabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) return [];
const hookSystem = config.getHookSystem();
@@ -214,8 +209,7 @@ function getHookDisplayName(hook: HookRegistryEntry): string {
async function enableAllAction(
context: CommandContext,
): Promise<void | MessageActionReturn> {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -286,8 +280,7 @@ async function enableAllAction(
async function disableAllAction(
context: CommandContext,
): Promise<void | MessageActionReturn> {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -60,12 +60,10 @@ describe('ideCommand', () => {
settings: {
setValue: vi.fn(),
},
agentContext: {
config: {
getIdeMode: vi.fn(),
setIdeMode: vi.fn(),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
},
config: {
getIdeMode: vi.fn(),
setIdeMode: vi.fn(),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
},
},
} as unknown as CommandContext;
+5 -15
View File
@@ -217,13 +217,9 @@ export const ideCommand = async (): Promise<SlashCommand> => {
);
// Poll for up to 5 seconds for the extension to activate.
for (let i = 0; i < 10; i++) {
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
true,
{
logToConsole: false,
},
);
await setIdeModeAndSyncConnection(context.services.config!, true, {
logToConsole: false,
});
if (
ideClient.getConnectionStatus().status ===
IDEConnectionStatus.Connected
@@ -266,10 +262,7 @@ export const ideCommand = async (): Promise<SlashCommand> => {
'ide.enabled',
true,
);
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
true,
);
await setIdeModeAndSyncConnection(context.services.config!, true);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(
{
@@ -292,10 +285,7 @@ export const ideCommand = async (): Promise<SlashCommand> => {
'ide.enabled',
false,
);
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
false,
);
await setIdeModeAndSyncConnection(context.services.config!, false);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(
{
@@ -31,10 +31,8 @@ describe('initCommand', () => {
// Create a fresh mock context for each test
mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getTargetDir: () => targetDir,
},
config: {
getTargetDir: () => targetDir,
},
},
});
@@ -96,7 +94,7 @@ describe('initCommand', () => {
// Arrange: Create a context without config
const noConfigContext = createMockCommandContext();
if (noConfigContext.services) {
noConfigContext.services.agentContext = null;
noConfigContext.services.config = null;
}
// Act: Run the command's action
+2 -2
View File
@@ -23,14 +23,14 @@ export const initCommand: SlashCommand = {
context: CommandContext,
_args: string,
): Promise<SlashCommandActionReturn> => {
if (!context.services.agentContext?.config) {
if (!context.services.config) {
return {
type: 'message',
messageType: 'error',
content: 'Configuration not available.',
};
}
const targetDir = context.services.agentContext.config.getTargetDir();
const targetDir = context.services.config.getTargetDir();
const geminiMdPath = path.join(targetDir, 'GEMINI.md');
const result = performInit(fs.existsSync(geminiMdPath));
@@ -119,10 +119,7 @@ describe('mcpCommand', () => {
mockContext = createMockCommandContext({
services: {
agentContext: {
config: mockConfig,
toolRegistry: mockConfig.getToolRegistry(),
},
config: mockConfig,
},
});
});
@@ -135,7 +132,7 @@ describe('mcpCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
agentContext: null,
config: null,
},
});
@@ -149,8 +146,7 @@ describe('mcpCommand', () => {
});
it('should show an error if tool registry is not available', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockContext.services.agentContext as any).toolRegistry = undefined;
mockConfig.getToolRegistry = vi.fn().mockReturnValue(undefined);
const result = await mcpCommand.action!(mockContext, '');
@@ -200,13 +196,9 @@ describe('mcpCommand', () => {
...mockServer3Tools,
];
const mockToolRegistry = {
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
getAllTools: vi.fn().mockReturnValue(allTools),
};
mockConfig.getToolRegistry = vi.fn().mockReturnValue(mockToolRegistry);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockContext.services.agentContext as any).toolRegistry =
mockToolRegistry;
});
const resourcesByServer: Record<
string,
+12 -17
View File
@@ -42,8 +42,8 @@ const authCommand: SlashCommand = {
args: string,
): Promise<MessageActionReturn> => {
const serverName = args.trim();
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -138,7 +138,7 @@ const authCommand: SlashCommand = {
await mcpClientManager.restartServer(serverName);
}
// Update the client with the new tools
const geminiClient = context.services.agentContext?.geminiClient;
const geminiClient = config.getGeminiClient();
if (geminiClient?.isInitialized()) {
await geminiClient.setTools();
}
@@ -162,8 +162,7 @@ const authCommand: SlashCommand = {
}
},
completion: async (context: CommandContext, partialArg: string) => {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) return [];
const mcpServers = config.getMcpClientManager()?.getMcpServers() || {};
@@ -178,8 +177,7 @@ const listAction = async (
showDescriptions = false,
showSchema = false,
): Promise<void | MessageActionReturn> => {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -190,7 +188,7 @@ const listAction = async (
config.setUserInteractedWithMcp();
const toolRegistry = agentContext.toolRegistry;
const toolRegistry = config.getToolRegistry();
if (!toolRegistry) {
return {
type: 'message',
@@ -336,8 +334,7 @@ const reloadCommand: SlashCommand = {
action: async (
context: CommandContext,
): Promise<void | SlashCommandActionReturn> => {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -363,7 +360,7 @@ const reloadCommand: SlashCommand = {
await mcpClientManager.restart();
// Update the client with the new tools
const geminiClient = agentContext.geminiClient;
const geminiClient = config.getGeminiClient();
if (geminiClient?.isInitialized()) {
await geminiClient.setTools();
}
@@ -380,8 +377,7 @@ async function handleEnableDisable(
args: string,
enable: boolean,
): Promise<MessageActionReturn> {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
return {
type: 'message',
@@ -469,8 +465,8 @@ async function handleEnableDisable(
);
await mcpClientManager.restart();
}
if (agentContext.geminiClient?.isInitialized())
await agentContext.geminiClient.setTools();
if (config.getGeminiClient()?.isInitialized())
await config.getGeminiClient().setTools();
context.ui.reloadCommands();
return { type: 'message', messageType: 'info', content: msg };
@@ -481,8 +477,7 @@ async function getEnablementCompletion(
partialArg: string,
showEnabled: boolean,
): Promise<string[]> {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) return [];
const servers = Object.keys(
config.getMcpClientManager()?.getMcpServers() || {},
@@ -102,12 +102,10 @@ describe('memoryCommand', () => {
mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getUserMemory: mockGetUserMemory,
getGeminiMdFileCount: mockGetGeminiMdFileCount,
getExtensionLoader: () => new SimpleExtensionLoader([]),
},
config: {
getUserMemory: mockGetUserMemory,
getGeminiMdFileCount: mockGetGeminiMdFileCount,
getExtensionLoader: () => new SimpleExtensionLoader([]),
},
},
});
@@ -252,7 +250,7 @@ describe('memoryCommand', () => {
mockContext = createMockCommandContext({
services: {
agentContext: { config: mockConfig },
config: mockConfig,
settings: {
merged: {
memoryDiscoveryMaxDirs: 1000,
@@ -270,7 +268,7 @@ describe('memoryCommand', () => {
if (!reloadCommand.action) throw new Error('Command has no action');
// Enable JIT in mock config
const config = mockContext.services.agentContext?.config;
const config = mockContext.services.config;
if (!config) throw new Error('Config is undefined');
vi.mocked(config.isJitContextEnabled).mockReturnValue(true);
@@ -372,7 +370,7 @@ describe('memoryCommand', () => {
if (!reloadCommand.action) throw new Error('Command has no action');
const nullConfigContext = createMockCommandContext({
services: { agentContext: null },
services: { config: null },
});
await expect(
@@ -415,10 +413,8 @@ describe('memoryCommand', () => {
});
mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
getGeminiMdFilePaths: mockGetGeminiMdfilePaths,
},
config: {
getGeminiMdFilePaths: mockGetGeminiMdfilePaths,
},
},
});
@@ -29,7 +29,7 @@ export const memoryCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
const config = context.services.agentContext?.config;
const config = context.services.config;
if (!config) return;
const result = showMemory(config);
@@ -81,7 +81,7 @@ export const memoryCommand: SlashCommand = {
);
try {
const config = context.services.agentContext?.config;
const config = context.services.config;
if (config) {
const result = await refreshMemory(config);
@@ -111,7 +111,7 @@ export const memoryCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
const config = context.services.agentContext?.config;
const config = context.services.config;
if (!config) return;
const result = listMemoryFiles(config);
@@ -37,11 +37,8 @@ describe('modelCommand', () => {
}
const mockRefreshUserQuota = vi.fn();
mockContext.services.agentContext = {
mockContext.services.config = {
refreshUserQuota: mockRefreshUserQuota,
get config() {
return this;
},
} as unknown as Config;
await modelCommand.action(mockContext, '');
@@ -69,11 +66,8 @@ describe('modelCommand', () => {
(c) => c.name === 'manage',
);
const mockRefreshUserQuota = vi.fn();
mockContext.services.agentContext = {
mockContext.services.config = {
refreshUserQuota: mockRefreshUserQuota,
get config() {
return this;
},
} as unknown as Config;
await manageCommand!.action!(mockContext, '');
@@ -90,7 +84,7 @@ describe('modelCommand', () => {
expect(setCommand).toBeDefined();
const mockSetModel = vi.fn();
mockContext.services.agentContext = {
mockContext.services.config = {
setModel: mockSetModel,
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
getUserId: vi.fn().mockReturnValue('test-user'),
@@ -104,9 +98,6 @@ describe('modelCommand', () => {
getPolicyEngine: vi.fn().mockReturnValue({
getApprovalMode: vi.fn().mockReturnValue('auto'),
}),
get config() {
return this;
},
} as unknown as Config;
await setCommand!.action!(mockContext, 'gemini-pro');
@@ -125,7 +116,7 @@ describe('modelCommand', () => {
(c) => c.name === 'set',
);
const mockSetModel = vi.fn();
mockContext.services.agentContext = {
mockContext.services.config = {
setModel: mockSetModel,
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
getUserId: vi.fn().mockReturnValue('test-user'),
@@ -139,9 +130,6 @@ describe('modelCommand', () => {
getPolicyEngine: vi.fn().mockReturnValue({
getApprovalMode: vi.fn().mockReturnValue('auto'),
}),
get config() {
return this;
},
} as unknown as Config;
await setCommand!.action!(mockContext, 'gemini-pro --persist');
+5 -5
View File
@@ -34,10 +34,10 @@ const setModelCommand: SlashCommand = {
const modelName = parts[0];
const persist = parts.includes('--persist');
if (context.services.agentContext?.config) {
context.services.agentContext.config.setModel(modelName, !persist);
if (context.services.config) {
context.services.config.setModel(modelName, !persist);
const event = new ModelSlashCommandEvent(modelName);
logModelSlashCommand(context.services.agentContext.config, event);
logModelSlashCommand(context.services.config, event);
context.ui.addItem({
type: MessageType.INFO,
@@ -53,8 +53,8 @@ const manageModelCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
if (context.services.agentContext?.config) {
await context.services.agentContext.config.refreshUserQuota();
if (context.services.config) {
await context.services.config.refreshUserQuota();
}
return {
type: 'dialog',
@@ -24,8 +24,7 @@ export const oncallCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, args): Promise<OpenCustomDialogActionReturn> => {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
throw new Error('Config not available');
}
@@ -57,8 +56,7 @@ export const oncallCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, args): Promise<OpenCustomDialogActionReturn> => {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
throw new Error('Config not available');
}
@@ -52,16 +52,14 @@ describe('planCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
agentContext: {
config: {
isPlanEnabled: vi.fn(),
setApprovalMode: vi.fn(),
getApprovedPlanPath: vi.fn(),
getApprovalMode: vi.fn(),
getFileSystemService: vi.fn(),
storage: {
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
},
config: {
isPlanEnabled: vi.fn(),
setApprovalMode: vi.fn(),
getApprovedPlanPath: vi.fn(),
getApprovalMode: vi.fn(),
getFileSystemService: vi.fn(),
storage: {
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
},
},
},
@@ -85,19 +83,17 @@ describe('planCommand', () => {
});
it('should switch to plan mode if enabled', async () => {
vi.mocked(
mockContext.services.agentContext!.config.isPlanEnabled,
).mockReturnValue(true);
vi.mocked(
mockContext.services.agentContext!.config.getApprovedPlanPath,
).mockReturnValue(undefined);
vi.mocked(mockContext.services.config!.isPlanEnabled).mockReturnValue(true);
vi.mocked(mockContext.services.config!.getApprovedPlanPath).mockReturnValue(
undefined,
);
if (!planCommand.action) throw new Error('Action missing');
await planCommand.action(mockContext, '');
expect(
mockContext.services.agentContext!.config.setApprovalMode,
).toHaveBeenCalledWith(ApprovalMode.PLAN);
expect(mockContext.services.config!.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.PLAN,
);
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'info',
'Switched to Plan Mode.',
@@ -106,12 +102,10 @@ describe('planCommand', () => {
it('should display the approved plan from config', async () => {
const mockPlanPath = '/mock/plans/dir/approved-plan.md';
vi.mocked(
mockContext.services.agentContext!.config.isPlanEnabled,
).mockReturnValue(true);
vi.mocked(
mockContext.services.agentContext!.config.getApprovedPlanPath,
).mockReturnValue(mockPlanPath);
vi.mocked(mockContext.services.config!.isPlanEnabled).mockReturnValue(true);
vi.mocked(mockContext.services.config!.getApprovedPlanPath).mockReturnValue(
mockPlanPath,
);
vi.mocked(processSingleFileContent).mockResolvedValue({
llmContent: '# Approved Plan Content',
returnDisplay: '# Approved Plan Content',
@@ -134,7 +128,7 @@ describe('planCommand', () => {
it('should copy the approved plan to clipboard', async () => {
const mockPlanPath = '/mock/plans/dir/approved-plan.md';
vi.mocked(
mockContext.services.agentContext!.config.getApprovedPlanPath,
mockContext.services.config!.getApprovedPlanPath,
).mockReturnValue(mockPlanPath);
vi.mocked(readFileWithEncoding).mockResolvedValue('# Plan Content');
@@ -155,7 +149,7 @@ describe('planCommand', () => {
it('should warn if no approved plan is found', async () => {
vi.mocked(
mockContext.services.agentContext!.config.getApprovedPlanPath,
mockContext.services.config!.getApprovedPlanPath,
).mockReturnValue(undefined);
const copySubCommand = planCommand.subCommands?.find(
+2 -2
View File
@@ -22,7 +22,7 @@ import * as path from 'node:path';
import { copyToClipboard } from '../utils/commandUtils.js';
async function copyAction(context: CommandContext) {
const config = context.services.agentContext?.config;
const config = context.services.config;
if (!config) {
debugLogger.debug('Plan copy command: config is not available in context');
return;
@@ -53,7 +53,7 @@ export const planCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: false,
action: async (context) => {
const config = context.services.agentContext?.config;
const config = context.services.config;
if (!config) {
debugLogger.debug('Plan command: config is not available in context');
return;
@@ -32,7 +32,7 @@ describe('policiesCommand', () => {
describe('list subcommand', () => {
it('should show error if config is missing', async () => {
mockContext.services.agentContext = null;
mockContext.services.config = null;
const listCommand = policiesCommand.subCommands![0];
await listCommand.action!(mockContext, '');
@@ -50,11 +50,8 @@ describe('policiesCommand', () => {
const mockPolicyEngine = {
getRules: vi.fn().mockReturnValue([]),
};
mockContext.services.agentContext = {
mockContext.services.config = {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
get config() {
return this;
},
} as unknown as Config;
const listCommand = policiesCommand.subCommands![0];
@@ -88,11 +85,8 @@ describe('policiesCommand', () => {
const mockPolicyEngine = {
getRules: vi.fn().mockReturnValue(mockRules),
};
mockContext.services.agentContext = {
mockContext.services.config = {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
get config() {
return this;
},
} as unknown as Config;
const listCommand = policiesCommand.subCommands![0];
@@ -116,9 +110,7 @@ describe('policiesCommand', () => {
expect(content).toContain(
'### Yolo Mode Policies (combined with normal mode policies)',
);
expect(content).toContain(
'### Plan Mode Policies (combined with normal mode policies)',
);
expect(content).toContain('### Plan Mode Policies');
expect(content).toContain(
'**DENY** tool: `dangerousTool` [Priority: 10]',
);
@@ -150,11 +142,8 @@ describe('policiesCommand', () => {
const mockPolicyEngine = {
getRules: vi.fn().mockReturnValue(mockRules),
};
mockContext.services.agentContext = {
mockContext.services.config = {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
get config() {
return this;
},
} as unknown as Config;
const listCommand = policiesCommand.subCommands![0];
@@ -164,9 +153,7 @@ describe('policiesCommand', () => {
const content = (call[0] as { text: string }).text;
// Plan-only rules appear under Plan Mode section
expect(content).toContain(
'### Plan Mode Policies (combined with normal mode policies)',
);
expect(content).toContain('### Plan Mode Policies');
// glob ALLOW is plan-only, should appear in plan section
expect(content).toContain('**ALLOW** tool: `glob` [Priority: 70]');
// shell ALLOW has no modes (applies to all), appears in normal section
@@ -51,8 +51,7 @@ const listPoliciesCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const { config } = context.services;
if (!config) {
context.ui.addItem(
{
@@ -100,10 +99,7 @@ const listPoliciesCommand: SlashCommand = {
'Yolo Mode Policies (combined with normal mode policies)',
uniqueYolo,
);
content += formatSection(
'Plan Mode Policies (combined with normal mode policies)',
uniquePlan,
);
content += formatSection('Plan Mode Policies', uniquePlan);
context.ui.addItem(
{
@@ -47,17 +47,14 @@ describe('restoreCommand', () => {
getProjectTempCheckpointsDir: vi.fn().mockReturnValue(checkpointsDir),
getProjectTempDir: vi.fn().mockReturnValue(geminiTempDir),
},
geminiClient: {
getGeminiClient: vi.fn().mockReturnValue({
setHistory: mockSetHistory,
},
get config() {
return this;
},
}),
} as unknown as Config;
mockContext = createMockCommandContext({
services: {
agentContext: mockConfig,
config: mockConfig,
git: mockGitService,
},
});
@@ -37,11 +37,10 @@ async function restoreAction(
args: string,
): Promise<void | SlashCommandActionReturn> {
const { services, ui } = context;
const { agentContext, git: gitService } = services;
const { config, git: gitService } = services;
const { addItem, loadHistory } = ui;
const checkpointDir =
agentContext?.config.storage.getProjectTempCheckpointsDir();
const checkpointDir = config?.storage.getProjectTempCheckpointsDir();
if (!checkpointDir) {
return {
@@ -117,7 +116,7 @@ async function restoreAction(
} else if (action.type === 'load_history' && loadHistory) {
loadHistory(action.history);
if (action.clientHistory) {
agentContext!.geminiClient?.setHistory(action.clientHistory);
config?.getGeminiClient()?.setHistory(action.clientHistory);
}
}
}
@@ -141,9 +140,8 @@ async function completion(
_partialArg: string,
): Promise<string[]> {
const { services } = context;
const { agentContext } = services;
const checkpointDir =
agentContext?.config.storage.getProjectTempCheckpointsDir();
const { config } = services;
const checkpointDir = config?.storage.getProjectTempCheckpointsDir();
if (!checkpointDir) {
return [];
}
@@ -97,17 +97,15 @@ describe('rewindCommand', () => {
mockContext = createMockCommandContext({
services: {
agentContext: {
geminiClient: {
config: {
getGeminiClient: () => ({
getChatRecordingService: mockGetChatRecordingService,
setHistory: mockSetHistory,
sendMessageStream: mockSendMessageStream,
},
config: {
getSessionId: () => 'test-session-id',
getContextManager: () => ({ refresh: mockResetContext }),
getProjectRoot: mockGetProjectRoot,
},
}),
getSessionId: () => 'test-session-id',
getContextManager: () => ({ refresh: mockResetContext }),
getProjectRoot: mockGetProjectRoot,
},
},
ui: {
@@ -295,12 +293,7 @@ describe('rewindCommand', () => {
it('should fail if client is not initialized', () => {
const context = createMockCommandContext({
services: {
agentContext: {
geminiClient: undefined,
get config() {
return this;
},
},
config: { getGeminiClient: () => undefined },
},
}) as unknown as CommandContext;
@@ -316,11 +309,8 @@ describe('rewindCommand', () => {
it('should fail if recording service is unavailable', () => {
const context = createMockCommandContext({
services: {
agentContext: {
geminiClient: { getChatRecordingService: () => undefined },
get config() {
return this;
},
config: {
getGeminiClient: () => ({ getChatRecordingService: () => undefined }),
},
},
}) as unknown as CommandContext;
@@ -61,7 +61,7 @@ async function rewindConversation(
client.setHistory(clientHistory as Content[]);
// Reset context manager as we are rewinding history
await context.services.agentContext?.config.getContextManager()?.refresh();
await context.services.config?.getContextManager()?.refresh();
// Update UI History
// We generate IDs based on index for the rewind history
@@ -94,8 +94,7 @@ export const rewindCommand: SlashCommand = {
description: 'Jump back to a specific message and restart the conversation',
kind: CommandKind.BUILT_IN,
action: (context) => {
const agentContext = context.services.agentContext;
const config = agentContext?.config;
const config = context.services.config;
if (!config)
return {
type: 'message',
@@ -103,7 +102,7 @@ export const rewindCommand: SlashCommand = {
content: 'Config not found',
};
const client = agentContext.geminiClient;
const client = config.getGeminiClient();
if (!client)
return {
type: 'message',
@@ -230,7 +230,7 @@ export const setupGithubCommand: SlashCommand = {
}
// Get the latest release tag from GitHub
const proxy = context?.services?.agentContext?.config.getProxy();
const proxy = context?.services?.config?.getProxy();
const releaseTag = await getLatestGitHubRelease(proxy);
const readmeUrl = `https://github.com/google-github-actions/run-gemini-cli/blob/${releaseTag}/README.md#quick-start`;
@@ -68,7 +68,7 @@ describe('skillsCommand', () => {
];
context = createMockCommandContext({
services: {
agentContext: {
config: {
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue(skills),
getSkills: vi.fn().mockReturnValue(skills),
@@ -80,9 +80,6 @@ describe('skillsCommand', () => {
),
}),
getContentGenerator: vi.fn(),
get config() {
return this;
},
} as unknown as Config,
settings: {
merged: createTestMergedSettings({ skills: { disabled: [] } }),
@@ -165,8 +162,7 @@ describe('skillsCommand', () => {
});
it('should filter built-in skills by default and show them with "all"', async () => {
const skillManager =
context.services.agentContext!.config.getSkillManager();
const skillManager = context.services.config!.getSkillManager();
const mockSkills = [
{
name: 'regular',
@@ -456,8 +452,7 @@ describe('skillsCommand', () => {
});
it('should show error if skills are disabled by admin during disable', async () => {
const skillManager =
context.services.agentContext!.config.getSkillManager();
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.isAdminEnabled).mockReturnValue(false);
const disableCmd = skillsCommand.subCommands!.find(
@@ -475,8 +470,7 @@ describe('skillsCommand', () => {
});
it('should show error if skills are disabled by admin during enable', async () => {
const skillManager =
context.services.agentContext!.config.getSkillManager();
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.isAdminEnabled).mockReturnValue(false);
const enableCmd = skillsCommand.subCommands!.find(
@@ -503,7 +497,8 @@ describe('skillsCommand', () => {
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 200));
});
context.services.agentContext!.config.reloadSkills = reloadSkillsMock;
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
const actionPromise = reloadCmd.action!(context, '');
@@ -542,15 +537,15 @@ describe('skillsCommand', () => {
(s) => s.name === 'reload',
)!;
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
const skillManager =
context.services.agentContext!.config.getSkillManager();
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.getSkills).mockReturnValue([
{ name: 'skill1' },
{ name: 'skill2' },
{ name: 'skill3' },
] as SkillDefinition[]);
});
context.services.agentContext!.config.reloadSkills = reloadSkillsMock;
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
await reloadCmd.action!(context, '');
@@ -567,13 +562,13 @@ describe('skillsCommand', () => {
(s) => s.name === 'reload',
)!;
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
const skillManager =
context.services.agentContext!.config.getSkillManager();
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.getSkills).mockReturnValue([
{ name: 'skill1' },
] as SkillDefinition[]);
});
context.services.agentContext!.config.reloadSkills = reloadSkillsMock;
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
await reloadCmd.action!(context, '');
@@ -590,14 +585,14 @@ describe('skillsCommand', () => {
(s) => s.name === 'reload',
)!;
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
const skillManager =
context.services.agentContext!.config.getSkillManager();
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.getSkills).mockReturnValue([
{ name: 'skill2' }, // skill1 removed, skill3 added
{ name: 'skill3' },
] as SkillDefinition[]);
});
context.services.agentContext!.config.reloadSkills = reloadSkillsMock;
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
await reloadCmd.action!(context, '');
@@ -613,7 +608,7 @@ describe('skillsCommand', () => {
const reloadCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'reload',
)!;
context.services.agentContext = null;
context.services.config = null;
await reloadCmd.action!(context, '');
@@ -633,7 +628,8 @@ describe('skillsCommand', () => {
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
await new Promise((_, reject) => setTimeout(() => reject(error), 200));
});
context.services.agentContext!.config.reloadSkills = reloadSkillsMock;
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
const actionPromise = reloadCmd.action!(context, '');
await vi.advanceTimersByTimeAsync(100);
@@ -655,8 +651,7 @@ describe('skillsCommand', () => {
const disableCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'disable',
)!;
const skillManager =
context.services.agentContext!.config.getSkillManager();
const skillManager = context.services.config!.getSkillManager();
const mockSkills = [
{
name: 'skill1',
@@ -686,8 +681,7 @@ describe('skillsCommand', () => {
const enableCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'enable',
)!;
const skillManager =
context.services.agentContext!.config.getSkillManager();
const skillManager = context.services.config!.getSkillManager();
const mockSkills = [
{
name: 'skill1',
+10 -10
View File
@@ -46,7 +46,7 @@ async function listAction(
}
}
const skillManager = context.services.agentContext?.config.getSkillManager();
const skillManager = context.services.config?.getSkillManager();
if (!skillManager) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -127,8 +127,8 @@ async function linkAction(
text: `Successfully linked skills from "${sourcePath}" (${scope}).`,
});
if (context.services.agentContext?.config) {
await context.services.agentContext.config.reloadSkills();
if (context.services.config) {
await context.services.config.reloadSkills();
}
} catch (error) {
context.ui.addItem({
@@ -150,14 +150,14 @@ async function disableAction(
});
return;
}
const skillManager = context.services.agentContext?.config.getSkillManager();
const skillManager = context.services.config?.getSkillManager();
if (skillManager?.isAdminEnabled() === false) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: getAdminErrorMessage(
'Agent skills',
context.services.agentContext?.config ?? undefined,
context.services.config ?? undefined,
),
},
Date.now(),
@@ -211,14 +211,14 @@ async function enableAction(
return;
}
const skillManager = context.services.agentContext?.config.getSkillManager();
const skillManager = context.services.config?.getSkillManager();
if (skillManager?.isAdminEnabled() === false) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: getAdminErrorMessage(
'Agent skills',
context.services.agentContext?.config ?? undefined,
context.services.config ?? undefined,
),
},
Date.now(),
@@ -246,7 +246,7 @@ async function enableAction(
async function reloadAction(
context: CommandContext,
): Promise<void | SlashCommandActionReturn> {
const config = context.services.agentContext?.config;
const config = context.services.config;
if (!config) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -333,7 +333,7 @@ function disableCompletion(
context: CommandContext,
partialArg: string,
): string[] {
const skillManager = context.services.agentContext?.config.getSkillManager();
const skillManager = context.services.config?.getSkillManager();
if (!skillManager) {
return [];
}
@@ -347,7 +347,7 @@ function enableCompletion(
context: CommandContext,
partialArg: string,
): string[] {
const skillManager = context.services.agentContext?.config.getSkillManager();
const skillManager = context.services.config?.getSkillManager();
if (!skillManager) {
return [];
}
@@ -43,15 +43,12 @@ describe('statsCommand', () => {
it('should display general session stats when run with no subcommand', async () => {
if (!statsCommand.action) throw new Error('Command has no action');
mockContext.services.agentContext = {
mockContext.services.config = {
refreshUserQuota: vi.fn(),
refreshAvailableCredits: vi.fn(),
getUserTierName: vi.fn(),
getUserPaidTier: vi.fn(),
getModel: vi.fn(),
get config() {
return this;
},
} as unknown as Config;
await statsCommand.action(mockContext, '');
@@ -83,7 +80,7 @@ describe('statsCommand', () => {
.fn()
.mockReturnValue('2025-01-01T12:00:00Z');
mockContext.services.agentContext = {
mockContext.services.config = {
refreshUserQuota: mockRefreshUserQuota,
getUserTierName: mockGetUserTierName,
getModel: mockGetModel,
@@ -92,9 +89,6 @@ describe('statsCommand', () => {
getQuotaResetTime: mockGetQuotaResetTime,
getUserPaidTier: vi.fn(),
refreshAvailableCredits: vi.fn(),
get config() {
return this;
},
} as unknown as Config;
await statsCommand.action(mockContext, '');
+13 -19
View File
@@ -29,8 +29,8 @@ function getUserIdentity(context: CommandContext) {
const cachedAccount = userAccountManager.getCachedGoogleAccount();
const userEmail = cachedAccount ?? undefined;
const tier = context.services.agentContext?.config.getUserTierName();
const paidTier = context.services.agentContext?.config.getUserPaidTier();
const tier = context.services.config?.getUserTierName();
const paidTier = context.services.config?.getUserPaidTier();
const creditBalance = getG1CreditBalance(paidTier) ?? undefined;
return { selectedAuthType, userEmail, tier, creditBalance };
@@ -50,7 +50,7 @@ async function defaultSessionView(context: CommandContext) {
const { selectedAuthType, userEmail, tier, creditBalance } =
getUserIdentity(context);
const currentModel = context.services.agentContext?.config.getModel();
const currentModel = context.services.config?.getModel();
const statsItem: HistoryItemStats = {
type: MessageType.STATS,
@@ -62,19 +62,16 @@ async function defaultSessionView(context: CommandContext) {
creditBalance,
};
if (context.services.agentContext?.config) {
if (context.services.config) {
const [quota] = await Promise.all([
context.services.agentContext.config.refreshUserQuota(),
context.services.agentContext.config.refreshAvailableCredits(),
context.services.config.refreshUserQuota(),
context.services.config.refreshAvailableCredits(),
]);
if (quota) {
statsItem.quotas = quota;
statsItem.pooledRemaining =
context.services.agentContext.config.getQuotaRemaining();
statsItem.pooledLimit =
context.services.agentContext.config.getQuotaLimit();
statsItem.pooledResetTime =
context.services.agentContext.config.getQuotaResetTime();
statsItem.pooledRemaining = context.services.config.getQuotaRemaining();
statsItem.pooledLimit = context.services.config.getQuotaLimit();
statsItem.pooledResetTime = context.services.config.getQuotaResetTime();
}
}
@@ -110,13 +107,10 @@ export const statsCommand: SlashCommand = {
isSafeConcurrent: true,
action: (context: CommandContext) => {
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
const currentModel = context.services.agentContext?.config.getModel();
const pooledRemaining =
context.services.agentContext?.config.getQuotaRemaining();
const pooledLimit =
context.services.agentContext?.config.getQuotaLimit();
const pooledResetTime =
context.services.agentContext?.config.getQuotaResetTime();
const currentModel = context.services.config?.getModel();
const pooledRemaining = context.services.config?.getQuotaRemaining();
const pooledLimit = context.services.config?.getQuotaLimit();
const pooledResetTime = context.services.config?.getQuotaResetTime();
context.ui.addItem({
type: MessageType.MODEL_STATS,
selectedAuthType,
@@ -30,8 +30,8 @@ describe('toolsCommand', () => {
it('should display an error if the tool registry is unavailable', async () => {
const mockContext = createMockCommandContext({
services: {
agentContext: {
toolRegistry: undefined,
config: {
getToolRegistry: () => undefined,
},
},
});
@@ -48,10 +48,10 @@ describe('toolsCommand', () => {
it('should display "No tools available" when none are found', async () => {
const mockContext = createMockCommandContext({
services: {
agentContext: {
toolRegistry: {
config: {
getToolRegistry: () => ({
getAllTools: () => [] as Array<ToolBuilder<object, ToolResult>>,
},
}),
},
},
});
@@ -69,8 +69,8 @@ describe('toolsCommand', () => {
it('should list tools without descriptions by default (no args)', async () => {
const mockContext = createMockCommandContext({
services: {
agentContext: {
toolRegistry: { getAllTools: () => mockTools },
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
},
},
});
@@ -90,8 +90,8 @@ describe('toolsCommand', () => {
it('should list tools without descriptions when "list" arg is passed', async () => {
const mockContext = createMockCommandContext({
services: {
agentContext: {
toolRegistry: { getAllTools: () => mockTools },
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
},
},
});
@@ -111,8 +111,8 @@ describe('toolsCommand', () => {
it('should list tools with descriptions when "desc" arg is passed', async () => {
const mockContext = createMockCommandContext({
services: {
agentContext: {
toolRegistry: { getAllTools: () => mockTools },
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
},
},
});
@@ -144,8 +144,8 @@ describe('toolsCommand', () => {
it('subcommand "list" should display tools without descriptions', async () => {
const mockContext = createMockCommandContext({
services: {
agentContext: {
toolRegistry: { getAllTools: () => mockTools },
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
},
},
});
@@ -165,8 +165,8 @@ describe('toolsCommand', () => {
it('subcommand "desc" should display tools with descriptions', async () => {
const mockContext = createMockCommandContext({
services: {
agentContext: {
toolRegistry: { getAllTools: () => mockTools },
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
},
},
});
@@ -196,8 +196,8 @@ describe('toolsCommand', () => {
const mockContext = createMockCommandContext({
services: {
agentContext: {
toolRegistry: { getAllTools: () => mockTools },
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
},
},
});
+1 -1
View File
@@ -15,7 +15,7 @@ async function listTools(
context: CommandContext,
showDescriptions: boolean,
): Promise<void> {
const toolRegistry = context.services.agentContext?.toolRegistry;
const toolRegistry = context.services.config?.getToolRegistry();
if (!toolRegistry) {
context.ui.addItem({
type: MessageType.ERROR,

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