mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 13:11:03 -07:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aee560c2eb | |||
| 2ef872e73a | |||
| b3014337d5 | |||
| 069641f65a | |||
| e36dfc9fc9 | |||
| 2ef6149684 | |||
| b70cf35df3 | |||
| f581ebdd19 | |||
| 54f24574e5 | |||
| 7f9808ce6b | |||
| 8429f559f0 | |||
| 9a947f5544 | |||
| d924ed0128 | |||
| 9f864ccd25 | |||
| b3ac1299e8 | |||
| 95b973e7fc | |||
| 13249bc518 | |||
| d1fcb09afa | |||
| f0473f3385 | |||
| 3d8edc5d97 | |||
| 6a2f2d3a91 | |||
| c32969c5a1 | |||
| 31a7a8187b | |||
| 70305907de | |||
| a2174751de | |||
| 8b762111a8 | |||
| c03d96b46c | |||
| ea1f19aa52 | |||
| 2eb1c92347 |
@@ -25,7 +25,7 @@ You are an expert at fixing behavioral evaluations.
|
||||
the same scenario. We don't want to lose test fidelity by making the prompts too
|
||||
direct (i.e.: easy).
|
||||
- Your primary mechanism for improving the agent's behavior is to make changes to
|
||||
tool instructions, prompt.ts, and/or modules that contribute to the prompt.
|
||||
tool instructions, system prompt (snippets.ts), and/or modules that contribute to the prompt.
|
||||
- If prompt and description changes are unsuccessful, use logs and debugging to
|
||||
confirm that everything is working as expected.
|
||||
- If unable to fix the test, you can make recommendations for architecture changes
|
||||
|
||||
@@ -59,7 +59,7 @@ Other ways to start in Plan Mode:
|
||||
You can enter Plan Mode in three ways:
|
||||
|
||||
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
|
||||
(`Default` -> `Plan` -> `Auto-Edit`).
|
||||
(`Default` -> `Auto-Edit` -> `Plan`).
|
||||
2. **Command:** Type `/plan` in the input box.
|
||||
3. **Natural Language:** Ask the agent to "start a plan for...".
|
||||
|
||||
|
||||
@@ -468,19 +468,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
}
|
||||
}
|
||||
},
|
||||
"flash-lite-helper": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0.2,
|
||||
"maxOutputTokens": 120,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit-corrector": {
|
||||
"extends": "base",
|
||||
"modelConfig": {
|
||||
|
||||
@@ -739,21 +739,10 @@ The MCP integration tracks several states:
|
||||
cautiously and only for servers you completely control
|
||||
- **Access tokens:** Be security-aware when configuring environment variables
|
||||
containing API keys or tokens
|
||||
- **Environment variable redaction:** By default, the Gemini CLI redacts
|
||||
sensitive environment variables (such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
|
||||
and variables matching patterns like `*TOKEN*`, `*SECRET*`, `*PASSWORD*`) when
|
||||
spawning MCP servers using the `stdio` transport. This prevents unintended
|
||||
exposure of your credentials to third-party servers.
|
||||
- **Explicit environment variables:** If you need to pass a specific environment
|
||||
variable to an MCP server, you should define it explicitly in the `env`
|
||||
property of the server configuration in `settings.json`.
|
||||
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
|
||||
available within the sandbox environment.
|
||||
available within the sandbox environment
|
||||
- **Private data:** Using broadly scoped personal access tokens can lead to
|
||||
information leakage between repositories.
|
||||
- **Untrusted servers:** Be extremely cautious when adding MCP servers from
|
||||
untrusted or third-party sources. Malicious servers could attempt to
|
||||
exfiltrate data or perform unauthorized actions through the tools they expose.
|
||||
|
||||
### Performance and resource management
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ const baseConfig = {
|
||||
write: true,
|
||||
};
|
||||
|
||||
const commonAliases = {
|
||||
punycode: 'punycode/',
|
||||
};
|
||||
|
||||
const cliConfig = {
|
||||
...baseConfig,
|
||||
banner: {
|
||||
@@ -88,6 +92,7 @@ const cliConfig = {
|
||||
plugins: createWasmPlugins(),
|
||||
alias: {
|
||||
'is-in-ci': path.resolve(__dirname, 'packages/cli/src/patches/is-in-ci.ts'),
|
||||
...commonAliases,
|
||||
},
|
||||
metafile: true,
|
||||
};
|
||||
@@ -103,6 +108,7 @@ const a2aServerConfig = {
|
||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||
},
|
||||
plugins: createWasmPlugins(),
|
||||
alias: commonAliases,
|
||||
};
|
||||
|
||||
Promise.allSettled([
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { it } from 'vitest';
|
||||
import { AppRig } from '../packages/cli/src/test-utils/AppRig.js';
|
||||
import type { EvalPolicy } from './test-helper.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { DEFAULT_GEMINI_MODEL } from '@google/gemini-cli-core';
|
||||
|
||||
export interface AppEvalCase {
|
||||
name: string;
|
||||
configOverrides?: any;
|
||||
prompt: string;
|
||||
timeout?: number;
|
||||
files?: Record<string, string>;
|
||||
setup?: (rig: AppRig) => Promise<void>;
|
||||
assert: (rig: AppRig, output: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for running behavioral evaluations using the in-process AppRig.
|
||||
* This matches the API of evalTest in test-helper.ts as closely as possible.
|
||||
*/
|
||||
export function appEvalTest(policy: EvalPolicy, evalCase: AppEvalCase) {
|
||||
const fn = async () => {
|
||||
const rig = new AppRig({
|
||||
configOverrides: {
|
||||
model: DEFAULT_GEMINI_MODEL,
|
||||
...evalCase.configOverrides,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await rig.initialize();
|
||||
|
||||
// Setup initial files
|
||||
if (evalCase.files) {
|
||||
const testDir = rig.getTestDir();
|
||||
for (const [filePath, content] of Object.entries(evalCase.files)) {
|
||||
const fullPath = path.join(testDir, filePath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
// Run custom setup if provided (e.g. for breakpoints)
|
||||
if (evalCase.setup) {
|
||||
await evalCase.setup(rig);
|
||||
}
|
||||
|
||||
// Render the app!
|
||||
rig.render();
|
||||
|
||||
// Wait for initial ready state
|
||||
await rig.waitForIdle();
|
||||
|
||||
// Send the initial prompt
|
||||
await rig.sendMessage(evalCase.prompt);
|
||||
|
||||
// Run assertion. Interaction-heavy tests can do their own waiting/steering here.
|
||||
await evalCase.assert(rig, rig.getStaticOutput());
|
||||
} finally {
|
||||
await rig.unmount();
|
||||
}
|
||||
};
|
||||
|
||||
if (policy === 'USUALLY_PASSES' && !process.env['RUN_EVALS']) {
|
||||
it.skip(evalCase.name, fn);
|
||||
} else {
|
||||
it(evalCase.name, fn, (evalCase.timeout ?? 60000) + 10000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('Edits location eval', () => {
|
||||
/**
|
||||
* Ensure that Gemini CLI always updates existing test files, if present,
|
||||
* instead of creating a new one.
|
||||
*/
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should update existing test file instead of creating a new one',
|
||||
files: {
|
||||
'package.json': JSON.stringify(
|
||||
{
|
||||
name: 'test-location-repro',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
test: 'vitest run',
|
||||
},
|
||||
devDependencies: {
|
||||
vitest: '^1.0.0',
|
||||
typescript: '^5.0.0',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
'src/math.ts': `
|
||||
export function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export function subtract(a: number, b: number): number {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
export function multiply(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
`,
|
||||
'src/math.test.ts': `
|
||||
import { expect, test } from 'vitest';
|
||||
import { add, subtract } from './math';
|
||||
|
||||
test('add adds two numbers', () => {
|
||||
expect(add(2, 3)).toBe(5);
|
||||
});
|
||||
|
||||
test('subtract subtracts two numbers', () => {
|
||||
expect(subtract(5, 3)).toBe(2);
|
||||
});
|
||||
`,
|
||||
'src/utils.ts': `
|
||||
export function capitalize(s: string): string {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
`,
|
||||
'src/utils.test.ts': `
|
||||
import { expect, test } from 'vitest';
|
||||
import { capitalize } from './utils';
|
||||
|
||||
test('capitalize capitalizes the first letter', () => {
|
||||
expect(capitalize('hello')).toBe('Hello');
|
||||
});
|
||||
`,
|
||||
},
|
||||
prompt: 'Fix the bug in src/math.ts. Do not run the code.',
|
||||
timeout: 180000,
|
||||
assert: async (rig) => {
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const replaceCalls = toolLogs.filter(
|
||||
(t) => t.toolRequest.name === 'replace',
|
||||
);
|
||||
const writeFileCalls = toolLogs.filter(
|
||||
(t) => t.toolRequest.name === 'write_file',
|
||||
);
|
||||
|
||||
expect(replaceCalls.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
writeFileCalls.some((file) =>
|
||||
file.toolRequest.args.includes('.test.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const targetFiles = replaceCalls.map((t) => {
|
||||
try {
|
||||
return JSON.parse(t.toolRequest.args).file_path;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('DEBUG: targetFiles', targetFiles);
|
||||
|
||||
expect(
|
||||
new Set(targetFiles).size,
|
||||
'Expected only two files changed',
|
||||
).greaterThanOrEqual(2);
|
||||
expect(targetFiles.some((f) => f?.endsWith('src/math.ts'))).toBe(true);
|
||||
expect(targetFiles.some((f) => f?.endsWith('src/math.test.ts'))).toBe(
|
||||
true,
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { appEvalTest } from './app-test-helper.js';
|
||||
import { PolicyDecision } from '@google/gemini-cli-core';
|
||||
|
||||
describe('Model Steering Behavioral Evals', () => {
|
||||
appEvalTest('ALWAYS_PASSES', {
|
||||
name: 'Corrective Hint: Model switches task based on hint during tool turn',
|
||||
configOverrides: {
|
||||
excludeTools: ['run_shell_command', 'ls', 'google_web_search'],
|
||||
},
|
||||
files: {
|
||||
'README.md':
|
||||
'# Gemini CLI\nThis is a tool for developers.\nLicense: Apache-2.0\nLine 4\nLine 5\nLine 6',
|
||||
},
|
||||
prompt: 'Find the first 5 lines of README.md',
|
||||
setup: async (rig) => {
|
||||
// Pause on any relevant tool to inject a corrective hint
|
||||
rig.setBreakpoint(['read_file', 'list_directory', 'glob']);
|
||||
},
|
||||
assert: async (rig) => {
|
||||
// Wait for the model to pause on any tool call
|
||||
await rig.waitForPendingConfirmation(
|
||||
/read_file|list_directory|glob/i,
|
||||
30000,
|
||||
);
|
||||
|
||||
// Interrupt with a corrective hint
|
||||
await rig.addUserHint(
|
||||
'Actually, stop what you are doing. Just tell me a short knock-knock joke about a robot instead.',
|
||||
);
|
||||
|
||||
// Resolve the tool to let the turn finish and the model see the hint
|
||||
await rig.resolveAwaitedTool();
|
||||
|
||||
// Verify the model pivots to the new task
|
||||
await rig.waitForOutput(/Knock,? knock/i, 40000);
|
||||
await rig.waitForIdle(30000);
|
||||
|
||||
const output = rig.getStaticOutput();
|
||||
expect(output).toMatch(/Knock,? knock/i);
|
||||
expect(output).not.toContain('Line 6');
|
||||
},
|
||||
});
|
||||
|
||||
appEvalTest('ALWAYS_PASSES', {
|
||||
name: 'Suggestive Hint: Model incorporates user guidance mid-stream',
|
||||
configOverrides: {
|
||||
excludeTools: ['run_shell_command', 'ls', 'google_web_search'],
|
||||
},
|
||||
files: {},
|
||||
prompt: 'Create a file called "hw.js" with a JS hello world.',
|
||||
setup: async (rig) => {
|
||||
// Pause on write_file to inject a suggestive hint
|
||||
rig.setBreakpoint(['write_file']);
|
||||
},
|
||||
assert: async (rig) => {
|
||||
// Wait for the model to start creating the first file
|
||||
await rig.waitForPendingConfirmation('write_file', 30000);
|
||||
|
||||
await rig.addUserHint(
|
||||
'Next, create a file called "hw.py" with a python hello world.',
|
||||
);
|
||||
|
||||
// Resolve and wait for the model to complete both tasks
|
||||
await rig.resolveAwaitedTool();
|
||||
await rig.waitForPendingConfirmation('write_file', 30000);
|
||||
await rig.resolveAwaitedTool();
|
||||
await rig.waitForIdle(60000);
|
||||
|
||||
const testDir = rig.getTestDir();
|
||||
const hwJs = path.join(testDir, 'hw.js');
|
||||
const hwPy = path.join(testDir, 'hw.py');
|
||||
|
||||
expect(fs.existsSync(hwJs), 'hw.js should exist').toBe(true);
|
||||
expect(fs.existsSync(hwPy), 'hw.py should exist').toBe(true);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -5,15 +5,8 @@
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
conditions: ['test'],
|
||||
},
|
||||
test: {
|
||||
testTimeout: 300000, // 5 minutes
|
||||
reporters: ['default', 'json'],
|
||||
@@ -21,16 +14,5 @@ export default defineConfig({
|
||||
json: 'evals/logs/report.json',
|
||||
},
|
||||
include: ['**/*.eval.ts'],
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
alias: {
|
||||
react: path.resolve(__dirname, '../node_modules/react'),
|
||||
},
|
||||
setupFiles: [path.resolve(__dirname, '../packages/cli/test-setup.ts')],
|
||||
server: {
|
||||
deps: {
|
||||
inline: [/@google\/gemini-cli-core/],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as os from 'node:os';
|
||||
import { RipGrepTool } from '../packages/core/src/tools/ripGrep.js';
|
||||
import { Config } from '../packages/core/src/config/config.js';
|
||||
import { WorkspaceContext } from '../packages/core/src/utils/workspaceContext.js';
|
||||
import { createMockMessageBus } from '../packages/core/src/test-utils/mock-message-bus.js';
|
||||
|
||||
// Mock Config to provide necessary context
|
||||
class MockConfig {
|
||||
@@ -66,7 +67,7 @@ describe('ripgrep-real-direct', () => {
|
||||
await fs.writeFile(path.join(tempDir, 'file3.txt'), 'goodbye moon\n');
|
||||
|
||||
const config = new MockConfig(tempDir) as unknown as Config;
|
||||
tool = new RipGrepTool(config);
|
||||
tool = new RipGrepTool(config, createMockMessageBus());
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -108,4 +109,24 @@ describe('ripgrep-real-direct', () => {
|
||||
expect(result.llmContent).toContain('script.js');
|
||||
expect(result.llmContent).not.toContain('file1.txt');
|
||||
});
|
||||
|
||||
it('should support context parameters', async () => {
|
||||
// Create a file with multiple lines
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, 'context.txt'),
|
||||
'line1\nline2\nline3 match\nline4\nline5\n',
|
||||
);
|
||||
|
||||
const invocation = tool.build({
|
||||
pattern: 'match',
|
||||
context: 1,
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result.llmContent).toContain('Found 1 match');
|
||||
expect(result.llmContent).toContain('context.txt');
|
||||
expect(result.llmContent).toContain('L2- line2');
|
||||
expect(result.llmContent).toContain('L3: line3 match');
|
||||
expect(result.llmContent).toContain('L4- line4');
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+29
-868
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
||||
"version": "0.29.5",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.0-nightly.20260203.71f46f116"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
@@ -129,6 +129,7 @@
|
||||
"ink": "npm:@jrichman/ink@6.4.8",
|
||||
"latest-version": "^9.0.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"punycode": "^2.3.1",
|
||||
"simple-git": "^3.28.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
||||
"version": "0.29.5",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
||||
"version": "0.29.5",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.0-nightly.20260203.71f46f116"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.29.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
@@ -34,8 +34,10 @@
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"ansi-escapes": "^7.3.0",
|
||||
"ansi-regex": "^6.2.2",
|
||||
"chalk": "^4.1.2",
|
||||
"cli-spinners": "^2.9.2",
|
||||
"clipboardy": "^5.0.0",
|
||||
"color-convert": "^2.0.1",
|
||||
"command-exists": "^1.2.9",
|
||||
@@ -56,7 +58,6 @@
|
||||
"prompts": "^2.4.2",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"react": "^19.2.0",
|
||||
"read-package-up": "^11.0.0",
|
||||
"shell-quote": "^1.8.3",
|
||||
"simple-git": "^3.28.0",
|
||||
"string-width": "^8.1.0",
|
||||
@@ -65,29 +66,21 @@
|
||||
"tar": "^7.5.2",
|
||||
"tinygradient": "^1.1.5",
|
||||
"undici": "^7.10.0",
|
||||
"wrap-ansi": "9.0.2",
|
||||
"ws": "^8.16.0",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@google/gemini-cli-test-utils": "file:../test-utils",
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/tar": "^6.1.13",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"archiver": "^7.0.1",
|
||||
"ink-testing-library": "^4.0.0",
|
||||
"pretty-format": "^30.0.2",
|
||||
"react-dom": "^19.2.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
|
||||
@@ -128,13 +128,6 @@ async function addMcpServer(
|
||||
|
||||
settings.setValue(settingsScope, 'mcpServers', mcpServers);
|
||||
|
||||
if (transport === 'stdio') {
|
||||
debugLogger.warn(
|
||||
'Security Warning: Running MCP servers with stdio transport can expose inherited environment variables. ' +
|
||||
'While the Gemini CLI redacts common API keys and secrets by default, you should only run servers from trusted sources.',
|
||||
);
|
||||
}
|
||||
|
||||
if (isExistingServer) {
|
||||
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
|
||||
} else {
|
||||
|
||||
@@ -445,7 +445,11 @@ export async function loadCliConfig(
|
||||
process.env['VITEST'] === 'true'
|
||||
? false
|
||||
: (settings.security?.folderTrust?.enabled ?? false);
|
||||
const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false;
|
||||
const trustedFolder =
|
||||
isWorkspaceTrusted(settings, cwd, undefined, {
|
||||
prompt: argv.prompt,
|
||||
query: argv.query,
|
||||
})?.isTrusted ?? false;
|
||||
|
||||
// Set the context filename in the server's memoryTool module BEFORE loading memory
|
||||
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
|
||||
@@ -602,8 +606,7 @@ export async function loadCliConfig(
|
||||
const interactive =
|
||||
!!argv.promptInteractive ||
|
||||
!!argv.experimentalAcp ||
|
||||
(!isHeadlessMode({ prompt: argv.prompt }) &&
|
||||
!argv.query &&
|
||||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
|
||||
!argv.isCommand);
|
||||
|
||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||
|
||||
@@ -449,6 +449,14 @@ describe('Trusted Folders', () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true for isPathTrusted when isHeadlessMode is true', async () => {
|
||||
const geminiCore = await import('@google/gemini-cli-core');
|
||||
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);
|
||||
|
||||
const folders = loadTrustedFolders();
|
||||
expect(folders.isPathTrusted('/any-untrusted-path')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trusted Folders Caching', () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
homedir,
|
||||
isHeadlessMode,
|
||||
coreEvents,
|
||||
type HeadlessModeOptions,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Settings } from './settings.js';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
@@ -128,7 +129,11 @@ export class LoadedTrustedFolders {
|
||||
isPathTrusted(
|
||||
location: string,
|
||||
config?: Record<string, TrustLevel>,
|
||||
headlessOptions?: HeadlessModeOptions,
|
||||
): boolean | undefined {
|
||||
if (isHeadlessMode(headlessOptions)) {
|
||||
return true;
|
||||
}
|
||||
const configToUse = config ?? this.user.config;
|
||||
|
||||
// Resolve location to its realpath for canonical comparison
|
||||
@@ -333,6 +338,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean {
|
||||
function getWorkspaceTrustFromLocalConfig(
|
||||
workspaceDir: string,
|
||||
trustConfig?: Record<string, TrustLevel>,
|
||||
headlessOptions?: HeadlessModeOptions,
|
||||
): TrustResult {
|
||||
const folders = loadTrustedFolders();
|
||||
const configToUse = trustConfig ?? folders.user.config;
|
||||
@@ -346,7 +352,11 @@ function getWorkspaceTrustFromLocalConfig(
|
||||
);
|
||||
}
|
||||
|
||||
const isTrusted = folders.isPathTrusted(workspaceDir, configToUse);
|
||||
const isTrusted = folders.isPathTrusted(
|
||||
workspaceDir,
|
||||
configToUse,
|
||||
headlessOptions,
|
||||
);
|
||||
return {
|
||||
isTrusted,
|
||||
source: isTrusted !== undefined ? 'file' : undefined,
|
||||
@@ -357,8 +367,9 @@ export function isWorkspaceTrusted(
|
||||
settings: Settings,
|
||||
workspaceDir: string = process.cwd(),
|
||||
trustConfig?: Record<string, TrustLevel>,
|
||||
headlessOptions?: HeadlessModeOptions,
|
||||
): TrustResult {
|
||||
if (isHeadlessMode()) {
|
||||
if (isHeadlessMode(headlessOptions)) {
|
||||
return { isTrusted: true, source: undefined };
|
||||
}
|
||||
|
||||
@@ -372,5 +383,9 @@ export function isWorkspaceTrusted(
|
||||
}
|
||||
|
||||
// Fall back to the local user configuration
|
||||
return getWorkspaceTrustFromLocalConfig(workspaceDir, trustConfig);
|
||||
return getWorkspaceTrustFromLocalConfig(
|
||||
workspaceDir,
|
||||
trustConfig,
|
||||
headlessOptions,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, afterEach } from 'vitest';
|
||||
import { AppRig } from '../test-utils/AppRig.js';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { PolicyDecision } from '@google/gemini-cli-core';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe('Model Steering Integration', () => {
|
||||
let rig: AppRig | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
await rig?.unmount();
|
||||
});
|
||||
|
||||
it('should steer the model using a hint during a tool turn', async () => {
|
||||
const fakeResponsesPath = path.join(
|
||||
__dirname,
|
||||
'../test-utils/fixtures/steering.responses',
|
||||
);
|
||||
rig = new AppRig({ fakeResponsesPath });
|
||||
await rig.initialize();
|
||||
rig.render();
|
||||
await rig.waitForIdle();
|
||||
|
||||
rig.setToolPolicy('list_directory', PolicyDecision.ASK_USER);
|
||||
rig.setToolPolicy('read_file', PolicyDecision.ASK_USER);
|
||||
|
||||
rig.setMockCommands([
|
||||
{
|
||||
command: /list_directory/,
|
||||
result: {
|
||||
output: 'file1.txt\nfile2.js\nfile3.md',
|
||||
exitCode: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
command: /read_file file1.txt/,
|
||||
result: {
|
||||
output: 'This is file1.txt content.',
|
||||
exitCode: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Start a long task
|
||||
await rig.type('Start long task');
|
||||
await rig.pressEnter();
|
||||
|
||||
// Wait for the model to call 'list_directory' (Confirming state)
|
||||
await rig.waitForOutput('ReadFolder');
|
||||
|
||||
// Injected a hint while the model is in a tool turn
|
||||
await rig.addUserHint('focus on .txt');
|
||||
|
||||
// Resolve list_directory (Proceed)
|
||||
await rig.resolveTool('ReadFolder');
|
||||
|
||||
// Wait for the model to process the hint and output the next action
|
||||
// Based on steering.responses, it should first acknowledge the hint
|
||||
await rig.waitForOutput('ACK: I will focus on .txt files now.');
|
||||
|
||||
// Then it should proceed with the next action
|
||||
await rig.waitForOutput(
|
||||
/Since you want me to focus on .txt files,[\s\S]*I will read file1.txt/,
|
||||
);
|
||||
await rig.waitForOutput('ReadFile');
|
||||
|
||||
// Resolve read_file (Proceed)
|
||||
await rig.resolveTool('ReadFile');
|
||||
|
||||
// Wait for final completion
|
||||
await rig.waitForOutput('Task complete.');
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, afterEach, expect } from 'vitest';
|
||||
import { AppRig } from './AppRig.js';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe('AppRig', () => {
|
||||
let rig: AppRig | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
await rig?.unmount();
|
||||
});
|
||||
|
||||
it('should handle deterministic tool turns with breakpoints', async () => {
|
||||
const fakeResponsesPath = path.join(
|
||||
__dirname,
|
||||
'fixtures',
|
||||
'steering.responses',
|
||||
);
|
||||
rig = new AppRig({ fakeResponsesPath });
|
||||
await rig.initialize();
|
||||
rig.render();
|
||||
await rig.waitForIdle();
|
||||
|
||||
// Set breakpoints on the canonical tool names
|
||||
rig.setBreakpoint('list_directory');
|
||||
rig.setBreakpoint('read_file');
|
||||
|
||||
// Start a task
|
||||
debugLogger.log('[Test] Sending message: Start long task');
|
||||
await rig.sendMessage('Start long task');
|
||||
|
||||
// Wait for the first breakpoint (list_directory)
|
||||
const pending1 = await rig.waitForPendingConfirmation('list_directory');
|
||||
expect(pending1.toolName).toBe('list_directory');
|
||||
|
||||
// Injected a hint
|
||||
await rig.addUserHint('focus on .txt');
|
||||
|
||||
// Resolve and wait for the NEXT breakpoint (read_file)
|
||||
// resolveTool will automatically remove the breakpoint policy for list_directory
|
||||
await rig.resolveTool('list_directory');
|
||||
|
||||
const pending2 = await rig.waitForPendingConfirmation('read_file');
|
||||
expect(pending2.toolName).toBe('read_file');
|
||||
|
||||
// Resolve and finish. Also removes read_file breakpoint.
|
||||
await rig.resolveTool('read_file');
|
||||
await rig.waitForOutput('Task complete.', 100000);
|
||||
});
|
||||
|
||||
it('should render the app and handle a simple message', async () => {
|
||||
const fakeResponsesPath = path.join(
|
||||
__dirname,
|
||||
'fixtures',
|
||||
'simple.responses',
|
||||
);
|
||||
rig = new AppRig({ fakeResponsesPath });
|
||||
await rig.initialize();
|
||||
rig.render();
|
||||
|
||||
// Wait for initial render
|
||||
await rig.waitForIdle();
|
||||
|
||||
// Type a message
|
||||
await rig.type('Hello');
|
||||
await rig.pressEnter();
|
||||
|
||||
// Wait for model response
|
||||
await rig.waitForOutput('Hello! How can I help you today?');
|
||||
});
|
||||
});
|
||||
@@ -1,569 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { AppContainer } from '../ui/AppContainer.js';
|
||||
import { renderWithProviders } from './render.js';
|
||||
import {
|
||||
makeFakeConfig,
|
||||
type Config,
|
||||
type ConfigParameters,
|
||||
ExtensionLoader,
|
||||
AuthType,
|
||||
ApprovalMode,
|
||||
createPolicyEngineConfig,
|
||||
PolicyDecision,
|
||||
ToolConfirmationOutcome,
|
||||
MessageBusType,
|
||||
type ToolCallsUpdateMessage,
|
||||
coreEvents,
|
||||
ideContextStore,
|
||||
createContentGenerator,
|
||||
startupProfiler,
|
||||
IdeClient,
|
||||
debugLogger,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type MockShellCommand,
|
||||
MockShellExecutionService,
|
||||
} from './MockShellExecutionService.js';
|
||||
import { createMockSettings } from './settings.js';
|
||||
import { type LoadedSettings } from '../config/settings.js';
|
||||
import { AuthState } from '../ui/types.js';
|
||||
|
||||
// Mock core functions globally for tests using AppRig.
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
const { MockShellExecutionService: MockService } = await import(
|
||||
'./MockShellExecutionService.js'
|
||||
);
|
||||
// Register the real execution logic so MockShellExecutionService can fall back to it
|
||||
MockService.setOriginalImplementation(original.ShellExecutionService.execute);
|
||||
|
||||
return {
|
||||
...original,
|
||||
ShellExecutionService: MockService,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock useAuthCommand to bypass authentication flows in tests
|
||||
vi.mock('../ui/auth/useAuth.js', () => ({
|
||||
useAuthCommand: () => ({
|
||||
authState: AuthState.Authenticated,
|
||||
setAuthState: vi.fn(),
|
||||
authError: null,
|
||||
onAuthError: vi.fn(),
|
||||
apiKeyDefaultValue: 'test-api-key',
|
||||
reloadApiKey: vi.fn().mockResolvedValue('test-api-key'),
|
||||
}),
|
||||
validateAuthMethodWithSettings: () => null,
|
||||
}));
|
||||
|
||||
// A minimal mock ExtensionManager to satisfy AppContainer's forceful cast
|
||||
class MockExtensionManager extends ExtensionLoader {
|
||||
getExtensions = vi.fn().mockReturnValue([]);
|
||||
setRequestConsent = vi.fn();
|
||||
setRequestSetting = vi.fn();
|
||||
}
|
||||
|
||||
export interface AppRigOptions {
|
||||
fakeResponsesPath?: string;
|
||||
terminalWidth?: number;
|
||||
terminalHeight?: number;
|
||||
configOverrides?: Partial<ConfigParameters>;
|
||||
}
|
||||
|
||||
export interface PendingConfirmation {
|
||||
toolName: string;
|
||||
toolDisplayName?: string;
|
||||
correlationId: string;
|
||||
}
|
||||
|
||||
export class AppRig {
|
||||
private renderResult: ReturnType<typeof renderWithProviders> | undefined;
|
||||
private config: Config | undefined;
|
||||
private settings: LoadedSettings | undefined;
|
||||
private testDir: string;
|
||||
private sessionId: string;
|
||||
|
||||
private pendingConfirmations = new Map<string, PendingConfirmation>();
|
||||
private breakpointTools = new Set<string | undefined>();
|
||||
private lastAwaitedConfirmation: PendingConfirmation | undefined;
|
||||
|
||||
constructor(private options: AppRigOptions = {}) {
|
||||
this.testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-app-rig-'));
|
||||
this.sessionId = `test-session-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.setupEnvironment();
|
||||
this.settings = this.createRigSettings();
|
||||
|
||||
const approvalMode =
|
||||
this.options.configOverrides?.approvalMode ?? ApprovalMode.DEFAULT;
|
||||
const policyEngineConfig = await createPolicyEngineConfig(
|
||||
this.settings.merged,
|
||||
approvalMode,
|
||||
);
|
||||
|
||||
const configParams: ConfigParameters = {
|
||||
sessionId: this.sessionId,
|
||||
targetDir: this.testDir,
|
||||
cwd: this.testDir,
|
||||
debugMode: false,
|
||||
model: 'test-model',
|
||||
fakeResponses: this.options.fakeResponsesPath,
|
||||
interactive: true,
|
||||
approvalMode,
|
||||
policyEngineConfig,
|
||||
enableEventDrivenScheduler: true,
|
||||
extensionLoader: new MockExtensionManager(),
|
||||
excludeTools: this.options.configOverrides?.excludeTools,
|
||||
...this.options.configOverrides,
|
||||
};
|
||||
this.config = makeFakeConfig(configParams);
|
||||
|
||||
if (this.options.fakeResponsesPath) {
|
||||
this.stubRefreshAuth();
|
||||
}
|
||||
|
||||
this.setupMessageBusListeners();
|
||||
|
||||
await act(async () => {
|
||||
await this.config!.initialize();
|
||||
// Since we mocked useAuthCommand, we must manually trigger the first
|
||||
// refreshAuth to ensure contentGenerator is initialized.
|
||||
await this.config!.refreshAuth(AuthType.USE_GEMINI);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private createRigSettings(): LoadedSettings {
|
||||
return createMockSettings({
|
||||
user: {
|
||||
path: path.join(this.testDir, '.gemini', 'user_settings.json'),
|
||||
settings: {
|
||||
security: {
|
||||
auth: {
|
||||
selectedType: AuthType.USE_GEMINI,
|
||||
useExternal: true,
|
||||
},
|
||||
folderTrust: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
ide: {
|
||||
enabled: false,
|
||||
hasSeenNudge: true,
|
||||
},
|
||||
},
|
||||
originalSettings: {},
|
||||
},
|
||||
merged: {
|
||||
security: {
|
||||
auth: {
|
||||
selectedType: AuthType.USE_GEMINI,
|
||||
useExternal: true,
|
||||
},
|
||||
folderTrust: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
ide: {
|
||||
enabled: false,
|
||||
hasSeenNudge: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private stubRefreshAuth() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any
|
||||
const gcConfig = this.config as any;
|
||||
gcConfig.refreshAuth = async (authMethod: AuthType) => {
|
||||
gcConfig.modelAvailabilityService.reset();
|
||||
|
||||
const newContentGeneratorConfig = {
|
||||
authType: authMethod,
|
||||
proxy: gcConfig.getProxy(),
|
||||
apiKey: process.env['GEMINI_API_KEY'] || 'test-api-key',
|
||||
};
|
||||
|
||||
gcConfig.contentGenerator = await createContentGenerator(
|
||||
newContentGeneratorConfig,
|
||||
this.config!,
|
||||
gcConfig.getSessionId(),
|
||||
);
|
||||
gcConfig.contentGeneratorConfig = newContentGeneratorConfig;
|
||||
|
||||
// Initialize BaseLlmClient now that the ContentGenerator is available
|
||||
const { BaseLlmClient } = await import('@google/gemini-cli-core');
|
||||
gcConfig.baseLlmClient = new BaseLlmClient(
|
||||
gcConfig.contentGenerator,
|
||||
this.config!,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private setupMessageBusListeners() {
|
||||
if (!this.config) return;
|
||||
const messageBus = this.config.getMessageBus();
|
||||
|
||||
messageBus.subscribe(
|
||||
MessageBusType.TOOL_CALLS_UPDATE,
|
||||
(message: ToolCallsUpdateMessage) => {
|
||||
for (const call of message.toolCalls) {
|
||||
if (call.status === 'awaiting_approval' && call.correlationId) {
|
||||
const details = call.confirmationDetails;
|
||||
const title = 'title' in details ? details.title : '';
|
||||
const toolDisplayName =
|
||||
call.tool?.displayName || title.replace(/^Confirm:\s*/, '');
|
||||
if (!this.pendingConfirmations.has(call.correlationId)) {
|
||||
this.pendingConfirmations.set(call.correlationId, {
|
||||
toolName: call.request.name,
|
||||
toolDisplayName,
|
||||
correlationId: call.correlationId,
|
||||
});
|
||||
}
|
||||
} else if (call.status !== 'awaiting_approval') {
|
||||
for (const [
|
||||
correlationId,
|
||||
pending,
|
||||
] of this.pendingConfirmations.entries()) {
|
||||
if (pending.toolName === call.request.name) {
|
||||
this.pendingConfirmations.delete(correlationId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.config || !this.settings)
|
||||
throw new Error('AppRig not initialized');
|
||||
|
||||
act(() => {
|
||||
this.renderResult = renderWithProviders(
|
||||
<AppContainer
|
||||
config={this.config!}
|
||||
version="test-version"
|
||||
initializationResult={{
|
||||
authError: null,
|
||||
themeError: null,
|
||||
shouldOpenAuthDialog: false,
|
||||
geminiMdFileCount: 0,
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
config: this.config!,
|
||||
settings: this.settings!,
|
||||
width: this.options.terminalWidth ?? 120,
|
||||
useAlternateBuffer: false,
|
||||
uiState: {
|
||||
terminalHeight: this.options.terminalHeight ?? 40,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setMockCommands(commands: MockShellCommand[]) {
|
||||
MockShellExecutionService.setMockCommands(commands);
|
||||
}
|
||||
|
||||
setToolPolicy(
|
||||
toolName: string | undefined,
|
||||
decision: PolicyDecision,
|
||||
priority = 10,
|
||||
) {
|
||||
if (!this.config) throw new Error('AppRig not initialized');
|
||||
this.config.getPolicyEngine().addRule({
|
||||
toolName,
|
||||
decision,
|
||||
priority,
|
||||
source: 'AppRig Override',
|
||||
});
|
||||
}
|
||||
|
||||
setBreakpoint(toolName: string | string[] | undefined) {
|
||||
if (Array.isArray(toolName)) {
|
||||
for (const name of toolName) {
|
||||
this.setBreakpoint(name);
|
||||
}
|
||||
} else {
|
||||
this.setToolPolicy(toolName, PolicyDecision.ASK_USER, 100);
|
||||
this.breakpointTools.add(toolName);
|
||||
}
|
||||
}
|
||||
|
||||
removeToolPolicy(toolName?: string, source = 'AppRig Override') {
|
||||
if (!this.config) throw new Error('AppRig not initialized');
|
||||
this.config
|
||||
.getPolicyEngine()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
.removeRulesForTool(toolName as string, source);
|
||||
this.breakpointTools.delete(toolName);
|
||||
}
|
||||
|
||||
getTestDir(): string {
|
||||
return this.testDir;
|
||||
}
|
||||
|
||||
getPendingConfirmations() {
|
||||
return Array.from(this.pendingConfirmations.values());
|
||||
}
|
||||
|
||||
private async waitUntil(
|
||||
predicate: () => boolean | Promise<boolean>,
|
||||
options: { timeout?: number; interval?: number; message?: string } = {},
|
||||
) {
|
||||
const {
|
||||
timeout = 30000,
|
||||
interval = 100,
|
||||
message = 'Condition timed out',
|
||||
} = options;
|
||||
const start = Date.now();
|
||||
|
||||
while (true) {
|
||||
if (await predicate()) return;
|
||||
|
||||
if (Date.now() - start > timeout) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async waitForPendingConfirmation(
|
||||
toolNameOrDisplayName?: string | RegExp,
|
||||
timeout = 30000,
|
||||
): Promise<PendingConfirmation> {
|
||||
const matches = (p: PendingConfirmation) => {
|
||||
if (!toolNameOrDisplayName) return true;
|
||||
if (typeof toolNameOrDisplayName === 'string') {
|
||||
return (
|
||||
p.toolName === toolNameOrDisplayName ||
|
||||
p.toolDisplayName === toolNameOrDisplayName
|
||||
);
|
||||
}
|
||||
return (
|
||||
toolNameOrDisplayName.test(p.toolName) ||
|
||||
toolNameOrDisplayName.test(p.toolDisplayName || '')
|
||||
);
|
||||
};
|
||||
|
||||
let matched: PendingConfirmation | undefined;
|
||||
await this.waitUntil(
|
||||
() => {
|
||||
matched = this.getPendingConfirmations().find(matches);
|
||||
return !!matched;
|
||||
},
|
||||
{
|
||||
timeout,
|
||||
message: `Timed out waiting for pending confirmation: ${toolNameOrDisplayName || 'any'}. Current pending: ${this.getPendingConfirmations()
|
||||
.map((p) => p.toolName)
|
||||
.join(', ')}`,
|
||||
},
|
||||
);
|
||||
|
||||
this.lastAwaitedConfirmation = matched;
|
||||
return matched!;
|
||||
}
|
||||
|
||||
async resolveTool(
|
||||
toolNameOrDisplayName: string | RegExp | PendingConfirmation,
|
||||
outcome: ToolConfirmationOutcome = ToolConfirmationOutcome.ProceedOnce,
|
||||
): Promise<void> {
|
||||
if (!this.config) throw new Error('AppRig not initialized');
|
||||
const messageBus = this.config.getMessageBus();
|
||||
|
||||
let pending: PendingConfirmation;
|
||||
if (
|
||||
typeof toolNameOrDisplayName === 'object' &&
|
||||
'correlationId' in toolNameOrDisplayName
|
||||
) {
|
||||
pending = toolNameOrDisplayName;
|
||||
} else {
|
||||
pending = await this.waitForPendingConfirmation(toolNameOrDisplayName);
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
this.pendingConfirmations.delete(pending.correlationId);
|
||||
|
||||
if (this.breakpointTools.has(pending.toolName)) {
|
||||
this.removeToolPolicy(pending.toolName);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
messageBus.publish({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: pending.correlationId,
|
||||
confirmed: outcome !== ToolConfirmationOutcome.Cancel,
|
||||
outcome,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
}
|
||||
|
||||
async resolveAwaitedTool(
|
||||
outcome: ToolConfirmationOutcome = ToolConfirmationOutcome.ProceedOnce,
|
||||
): Promise<void> {
|
||||
if (!this.lastAwaitedConfirmation) {
|
||||
throw new Error('No tool has been awaited yet');
|
||||
}
|
||||
await this.resolveTool(this.lastAwaitedConfirmation, outcome);
|
||||
this.lastAwaitedConfirmation = undefined;
|
||||
}
|
||||
|
||||
async addUserHint(hint: string) {
|
||||
if (!this.config) throw new Error('AppRig not initialized');
|
||||
await act(async () => {
|
||||
this.config!.addUserHint(hint);
|
||||
});
|
||||
}
|
||||
|
||||
getConfig(): Config {
|
||||
if (!this.config) throw new Error('AppRig not initialized');
|
||||
return this.config;
|
||||
}
|
||||
|
||||
async type(text: string) {
|
||||
if (!this.renderResult) throw new Error('AppRig not initialized');
|
||||
await act(async () => {
|
||||
this.renderResult!.stdin.write(text);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
}
|
||||
|
||||
async pressEnter() {
|
||||
await this.type('\r');
|
||||
}
|
||||
|
||||
async pressKey(key: string) {
|
||||
if (!this.renderResult) throw new Error('AppRig not initialized');
|
||||
await act(async () => {
|
||||
this.renderResult!.stdin.write(key);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
}
|
||||
|
||||
get lastFrame() {
|
||||
if (!this.renderResult) return '';
|
||||
return stripAnsi(this.renderResult.lastFrame() || '');
|
||||
}
|
||||
|
||||
getStaticOutput() {
|
||||
if (!this.renderResult) return '';
|
||||
return stripAnsi(this.renderResult.stdout.lastFrame() || '');
|
||||
}
|
||||
|
||||
async waitForOutput(pattern: string | RegExp, timeout = 30000) {
|
||||
await this.waitUntil(
|
||||
() => {
|
||||
const frame = this.lastFrame;
|
||||
return typeof pattern === 'string'
|
||||
? frame.includes(pattern)
|
||||
: pattern.test(frame);
|
||||
},
|
||||
{
|
||||
timeout,
|
||||
message: `Timed out waiting for output: ${pattern}\nLast frame:\n${this.lastFrame}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async waitForIdle(timeout = 20000) {
|
||||
await this.waitForOutput('Type your message', timeout);
|
||||
}
|
||||
|
||||
async sendMessage(text: string) {
|
||||
await this.type(text);
|
||||
await this.pressEnter();
|
||||
}
|
||||
|
||||
async unmount() {
|
||||
// Poison the chat recording service to prevent late writes to the test directory
|
||||
if (this.config) {
|
||||
const recordingService = this.config
|
||||
.getGeminiClient()
|
||||
?.getChatRecordingService();
|
||||
if (recordingService) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
|
||||
(recordingService as any).conversationFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.renderResult) {
|
||||
this.renderResult.unmount();
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
});
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
|
||||
coreEvents.removeAllListeners();
|
||||
coreEvents.drainBacklogs();
|
||||
MockShellExecutionService.reset();
|
||||
ideContextStore.clear();
|
||||
// Forcefully clear IdeClient singleton promise
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
|
||||
(IdeClient as any).instancePromise = null;
|
||||
startupProfiler.clear();
|
||||
vi.clearAllMocks();
|
||||
|
||||
this.config = undefined;
|
||||
this.renderResult = undefined;
|
||||
|
||||
if (this.testDir && fs.existsSync(this.testDir)) {
|
||||
try {
|
||||
fs.rmSync(this.testDir, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
debugLogger.warn(
|
||||
`Failed to cleanup test directory ${this.testDir}:`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
import type {
|
||||
ShellExecutionHandle,
|
||||
ShellExecutionResult,
|
||||
ShellOutputEvent,
|
||||
ShellExecutionConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export interface MockShellCommand {
|
||||
command: string | RegExp;
|
||||
result: Partial<ShellExecutionResult>;
|
||||
events?: ShellOutputEvent[];
|
||||
}
|
||||
|
||||
type ShellExecutionServiceExecute = (
|
||||
commandToExecute: string,
|
||||
cwd: string,
|
||||
onOutputEvent: (event: ShellOutputEvent) => void,
|
||||
abortSignal: AbortSignal,
|
||||
shouldUseNodePty: boolean,
|
||||
shellExecutionConfig: ShellExecutionConfig,
|
||||
) => Promise<ShellExecutionHandle>;
|
||||
|
||||
export class MockShellExecutionService {
|
||||
private static mockCommands: MockShellCommand[] = [];
|
||||
private static originalExecute: ShellExecutionServiceExecute | undefined;
|
||||
private static passthroughEnabled = false;
|
||||
|
||||
/**
|
||||
* Registers the original implementation to allow falling back to real shell execution.
|
||||
*/
|
||||
static setOriginalImplementation(
|
||||
implementation: ShellExecutionServiceExecute,
|
||||
) {
|
||||
this.originalExecute = implementation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables passthrough to the real implementation when no mock matches.
|
||||
*/
|
||||
static setPassthrough(enabled: boolean) {
|
||||
this.passthroughEnabled = enabled;
|
||||
}
|
||||
|
||||
static setMockCommands(commands: MockShellCommand[]) {
|
||||
this.mockCommands = commands;
|
||||
}
|
||||
|
||||
static reset() {
|
||||
this.mockCommands = [];
|
||||
this.passthroughEnabled = false;
|
||||
this.writeToPty.mockClear();
|
||||
this.kill.mockClear();
|
||||
this.background.mockClear();
|
||||
this.resizePty.mockClear();
|
||||
this.scrollPty.mockClear();
|
||||
}
|
||||
|
||||
static async execute(
|
||||
commandToExecute: string,
|
||||
cwd: string,
|
||||
onOutputEvent: (event: ShellOutputEvent) => void,
|
||||
abortSignal: AbortSignal,
|
||||
shouldUseNodePty: boolean,
|
||||
shellExecutionConfig: ShellExecutionConfig,
|
||||
): Promise<ShellExecutionHandle> {
|
||||
const mock = this.mockCommands.find((m) =>
|
||||
typeof m.command === 'string'
|
||||
? m.command === commandToExecute
|
||||
: m.command.test(commandToExecute),
|
||||
);
|
||||
|
||||
const pid = Math.floor(Math.random() * 10000);
|
||||
|
||||
if (mock) {
|
||||
if (mock.events) {
|
||||
for (const event of mock.events) {
|
||||
onOutputEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
const result: ShellExecutionResult = {
|
||||
rawOutput: Buffer.from(mock.result.output || ''),
|
||||
output: mock.result.output || '',
|
||||
exitCode: mock.result.exitCode ?? 0,
|
||||
signal: mock.result.signal ?? null,
|
||||
error: mock.result.error ?? null,
|
||||
aborted: false,
|
||||
pid,
|
||||
executionMethod: 'none',
|
||||
...mock.result,
|
||||
};
|
||||
|
||||
return {
|
||||
pid,
|
||||
result: Promise.resolve(result),
|
||||
};
|
||||
}
|
||||
|
||||
if (this.passthroughEnabled && this.originalExecute) {
|
||||
return this.originalExecute(
|
||||
commandToExecute,
|
||||
cwd,
|
||||
onOutputEvent,
|
||||
abortSignal,
|
||||
shouldUseNodePty,
|
||||
shellExecutionConfig,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
pid,
|
||||
result: Promise.resolve({
|
||||
rawOutput: Buffer.from(''),
|
||||
output: `Command not found: ${commandToExecute}`,
|
||||
exitCode: 127,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid,
|
||||
executionMethod: 'none',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
static writeToPty = vi.fn();
|
||||
static isPtyActive = vi.fn(() => false);
|
||||
static onExit = vi.fn(() => () => {});
|
||||
static kill = vi.fn();
|
||||
static background = vi.fn();
|
||||
static subscribe = vi.fn(() => () => {});
|
||||
static resizePty = vi.fn();
|
||||
static scrollPty = vi.fn();
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Hello! How can I help you today?"}],"role":"model"},"finishReason":"STOP"}]}]}
|
||||
@@ -1,4 +0,0 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"Starting a long task. First, I'll list the files."},{"functionCall":{"name":"list_directory","args":{"dir_path":"."}}}]},"finishReason":"STOP"}]}]}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"role":"model","parts":[{"text":"ACK: I will focus on .txt files now."}]},"finishReason":"STOP"}]}}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I see the files. Since you want me to focus on .txt files, I will read file1.txt."},{"functionCall":{"name":"read_file","args":{"file_path":"file1.txt"}}}]},"finishReason":"STOP"}]}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I have read file1.txt. Task complete."}]},"finishReason":"STOP"}]}]}
|
||||
@@ -33,7 +33,6 @@ 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';
|
||||
import { SessionStatsProvider } from '../ui/contexts/SessionContext.js';
|
||||
|
||||
export const persistentStateMock = new FakePersistentState();
|
||||
|
||||
@@ -161,8 +160,6 @@ const baseMockUiState = {
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
},
|
||||
hintMode: false,
|
||||
hintBuffer: '',
|
||||
};
|
||||
|
||||
export const mockAppState: AppState = {
|
||||
@@ -212,10 +209,6 @@ const mockUIActions: UIActions = {
|
||||
setActiveBackgroundShellPid: vi.fn(),
|
||||
setIsBackgroundShellListOpen: vi.fn(),
|
||||
setAuthContext: vi.fn(),
|
||||
onHintInput: vi.fn(),
|
||||
onHintBackspace: vi.fn(),
|
||||
onHintClear: vi.fn(),
|
||||
onHintSubmit: vi.fn(),
|
||||
handleRestart: vi.fn(),
|
||||
handleNewAgentsSelect: vi.fn(),
|
||||
};
|
||||
@@ -313,43 +306,39 @@ export const renderWithProviders = (
|
||||
<UIStateContext.Provider value={finalUiState}>
|
||||
<VimModeProvider settings={finalSettings}>
|
||||
<ShellFocusContext.Provider value={shellFocus}>
|
||||
<SessionStatsProvider>
|
||||
<StreamingContext.Provider
|
||||
value={finalUiState.streamingState}
|
||||
>
|
||||
<UIActionsContext.Provider value={finalUIActions}>
|
||||
<ToolActionsProvider
|
||||
config={config}
|
||||
toolCalls={allToolCalls}
|
||||
<StreamingContext.Provider value={finalUiState.streamingState}>
|
||||
<UIActionsContext.Provider value={finalUIActions}>
|
||||
<ToolActionsProvider
|
||||
config={config}
|
||||
toolCalls={allToolCalls}
|
||||
>
|
||||
<AskUserActionsProvider
|
||||
request={null}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
>
|
||||
<AskUserActionsProvider
|
||||
request={null}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
>
|
||||
<KeypressProvider>
|
||||
<MouseProvider
|
||||
mouseEventsEnabled={mouseEventsEnabled}
|
||||
>
|
||||
<TerminalProvider>
|
||||
<ScrollProvider>
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
flexDirection="column"
|
||||
>
|
||||
{component}
|
||||
</Box>
|
||||
</ScrollProvider>
|
||||
</TerminalProvider>
|
||||
</MouseProvider>
|
||||
</KeypressProvider>
|
||||
</AskUserActionsProvider>
|
||||
</ToolActionsProvider>
|
||||
</UIActionsContext.Provider>
|
||||
</StreamingContext.Provider>
|
||||
</SessionStatsProvider>
|
||||
<KeypressProvider>
|
||||
<MouseProvider
|
||||
mouseEventsEnabled={mouseEventsEnabled}
|
||||
>
|
||||
<TerminalProvider>
|
||||
<ScrollProvider>
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
flexDirection="column"
|
||||
>
|
||||
{component}
|
||||
</Box>
|
||||
</ScrollProvider>
|
||||
</TerminalProvider>
|
||||
</MouseProvider>
|
||||
</KeypressProvider>
|
||||
</AskUserActionsProvider>
|
||||
</ToolActionsProvider>
|
||||
</UIActionsContext.Provider>
|
||||
</StreamingContext.Provider>
|
||||
</ShellFocusContext.Provider>
|
||||
</VimModeProvider>
|
||||
</UIStateContext.Provider>
|
||||
|
||||
@@ -94,10 +94,6 @@ import { basename } from 'node:path';
|
||||
import { computeTerminalTitle } from '../utils/windowTitle.js';
|
||||
import { useTextBuffer } from './components/shared/text-buffer.js';
|
||||
import { useLogger } from './hooks/useLogger.js';
|
||||
import {
|
||||
buildUserSteeringHintPrompt,
|
||||
generateSteeringAckMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useGeminiStream } from './hooks/useGeminiStream.js';
|
||||
import { type BackgroundShell } from './hooks/shellCommandProcessor.js';
|
||||
import { useVim } from './hooks/vim.js';
|
||||
@@ -607,7 +603,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
apiKeyDefaultValue,
|
||||
reloadApiKey,
|
||||
} = useAuthCommand(settings, config, initializationResult.authError);
|
||||
|
||||
const [authContext, setAuthContext] = useState<{ requiresRestart?: boolean }>(
|
||||
{},
|
||||
);
|
||||
@@ -676,6 +671,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
settings.setValue(scope, 'security.auth.selectedType', authType);
|
||||
|
||||
try {
|
||||
config.setRemoteAdminSettings(undefined);
|
||||
await config.refreshAuth(authType);
|
||||
setAuthState(AuthState.Authenticated);
|
||||
} catch (e) {
|
||||
@@ -968,19 +964,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
}, [pendingRestorePrompt, inputHistory, historyManager.history]);
|
||||
|
||||
const lastProcessedHintIndexRef = useRef<number>(-1);
|
||||
|
||||
const consumePendingHints = useCallback(() => {
|
||||
const userHints = config.getUserHintsAfter(
|
||||
lastProcessedHintIndexRef.current,
|
||||
);
|
||||
if (userHints.length === 0) {
|
||||
return null;
|
||||
}
|
||||
lastProcessedHintIndexRef.current = config.getLatestHintIndex();
|
||||
return userHints.join('\n');
|
||||
}, [config]);
|
||||
|
||||
const {
|
||||
streamingState,
|
||||
submitQuery,
|
||||
@@ -1019,7 +1002,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
embeddedShellFocused,
|
||||
consumePendingHints,
|
||||
);
|
||||
|
||||
toggleBackgroundShellRef.current = toggleBackgroundShell;
|
||||
@@ -1122,38 +1104,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
],
|
||||
);
|
||||
|
||||
const handleHintSubmit = useCallback(
|
||||
(hint: string) => {
|
||||
const trimmed = hint.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
config.addUserHint(trimmed);
|
||||
// Render hints with a distinct style.
|
||||
historyManager.addItem({
|
||||
type: 'hint',
|
||||
text: trimmed,
|
||||
} as Omit<HistoryItem, 'id'>);
|
||||
},
|
||||
[config, historyManager],
|
||||
);
|
||||
|
||||
const handleFinalSubmit = useCallback(
|
||||
async (submittedValue: string) => {
|
||||
const isSlash = isSlashCommand(submittedValue.trim());
|
||||
const isIdle = streamingState === StreamingState.Idle;
|
||||
const isAgentRunning =
|
||||
streamingState === StreamingState.Responding ||
|
||||
isToolExecuting([
|
||||
...pendingSlashCommandHistoryItems,
|
||||
...pendingGeminiHistoryItems,
|
||||
]);
|
||||
|
||||
if (isAgentRunning && !isSlash) {
|
||||
handleHintSubmit(submittedValue);
|
||||
addInput(submittedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSlash || (isIdle && isMcpReady)) {
|
||||
if (!isSlash) {
|
||||
@@ -1195,10 +1149,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isMcpReady,
|
||||
streamingState,
|
||||
messageQueue.length,
|
||||
pendingSlashCommandHistoryItems,
|
||||
pendingGeminiHistoryItems,
|
||||
config,
|
||||
handleHintSubmit,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1864,45 +1815,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isConfigInitialized ||
|
||||
streamingState !== StreamingState.Idle ||
|
||||
!isMcpReady ||
|
||||
isToolAwaitingConfirmation(pendingHistoryItems)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingHint = consumePendingHints();
|
||||
if (!pendingHint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const geminiClient = config.getGeminiClient();
|
||||
void generateSteeringAckMessage(geminiClient, pendingHint).then(
|
||||
(ackText) => {
|
||||
historyManager.addItem({
|
||||
type: 'info',
|
||||
icon: '· ',
|
||||
color: 'gray',
|
||||
marginBottom: 1,
|
||||
text: ackText,
|
||||
} as Omit<HistoryItem, 'id'>);
|
||||
},
|
||||
);
|
||||
void submitQuery([{ text: buildUserSteeringHintPrompt(pendingHint) }]);
|
||||
}, [
|
||||
config,
|
||||
historyManager,
|
||||
isConfigInitialized,
|
||||
isMcpReady,
|
||||
streamingState,
|
||||
submitQuery,
|
||||
consumePendingHints,
|
||||
pendingHistoryItems,
|
||||
]);
|
||||
|
||||
const allToolCalls = useMemo(
|
||||
() =>
|
||||
pendingHistoryItems
|
||||
@@ -2064,8 +1976,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isBackgroundShellListOpen,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
hintMode: false,
|
||||
hintBuffer: '',
|
||||
}),
|
||||
[
|
||||
isThemeDialogOpen,
|
||||
@@ -2228,10 +2138,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setActiveBackgroundShellPid,
|
||||
setIsBackgroundShellListOpen,
|
||||
setAuthContext,
|
||||
onHintInput: () => {},
|
||||
onHintBackspace: () => {},
|
||||
onHintClear: () => {},
|
||||
onHintSubmit: () => {},
|
||||
handleRestart: async () => {
|
||||
if (process.send) {
|
||||
const remoteSettings = config.getRemoteAdminSettings();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { getDisplayString } from '@google/gemini-cli-core';
|
||||
|
||||
interface AboutBoxProps {
|
||||
cliVersion: string;
|
||||
@@ -79,7 +80,9 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>{modelVersion}</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
{getDisplayString(modelVersion)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
|
||||
@@ -14,9 +14,7 @@ describe('ApprovalModeIndicator', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('auto-accept edits');
|
||||
expect(output).toContain('shift+tab to manual');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for AUTO_EDIT mode with plan enabled', () => {
|
||||
@@ -26,35 +24,28 @@ describe('ApprovalModeIndicator', () => {
|
||||
isPlanEnabled={true}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('auto-accept edits');
|
||||
expect(output).toContain('shift+tab to manual');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for PLAN mode', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.PLAN} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('plan');
|
||||
expect(output).toContain('shift+tab to accept edits');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for YOLO mode', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('YOLO');
|
||||
expect(output).toContain('ctrl+y');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for DEFAULT mode', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.DEFAULT} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('shift+tab to accept edits');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for DEFAULT mode with plan enabled', () => {
|
||||
@@ -64,7 +55,6 @@ describe('ApprovalModeIndicator', () => {
|
||||
isPlanEnabled={true}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('shift+tab to plan');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,16 @@ interface ApprovalModeIndicatorProps {
|
||||
isPlanEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const APPROVAL_MODE_TEXT = {
|
||||
AUTO_EDIT: 'auto-accept edits',
|
||||
PLAN: 'plan',
|
||||
YOLO: 'YOLO',
|
||||
HINT_SWITCH_TO_PLAN_MODE: 'shift+tab to plan',
|
||||
HINT_SWITCH_TO_MANUAL_MODE: 'shift+tab to manual',
|
||||
HINT_SWITCH_TO_AUTO_EDIT_MODE: 'shift+tab to accept edits',
|
||||
HINT_SWITCH_TO_YOLO_MODE: 'ctrl+y',
|
||||
};
|
||||
|
||||
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
||||
approvalMode,
|
||||
isPlanEnabled,
|
||||
@@ -25,26 +35,26 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
||||
switch (approvalMode) {
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
textColor = theme.status.warning;
|
||||
textContent = 'auto-accept edits';
|
||||
subText = 'shift+tab to manual';
|
||||
textContent = APPROVAL_MODE_TEXT.AUTO_EDIT;
|
||||
subText = isPlanEnabled
|
||||
? APPROVAL_MODE_TEXT.HINT_SWITCH_TO_PLAN_MODE
|
||||
: APPROVAL_MODE_TEXT.HINT_SWITCH_TO_MANUAL_MODE;
|
||||
break;
|
||||
case ApprovalMode.PLAN:
|
||||
textColor = theme.status.success;
|
||||
textContent = 'plan';
|
||||
subText = 'shift+tab to accept edits';
|
||||
textContent = APPROVAL_MODE_TEXT.PLAN;
|
||||
subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_MANUAL_MODE;
|
||||
break;
|
||||
case ApprovalMode.YOLO:
|
||||
textColor = theme.status.error;
|
||||
textContent = 'YOLO';
|
||||
subText = 'ctrl+y';
|
||||
textContent = APPROVAL_MODE_TEXT.YOLO;
|
||||
subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_YOLO_MODE;
|
||||
break;
|
||||
case ApprovalMode.DEFAULT:
|
||||
default:
|
||||
textColor = theme.text.accent;
|
||||
textContent = '';
|
||||
subText = isPlanEnabled
|
||||
? 'shift+tab to plan'
|
||||
: 'shift+tab to accept edits';
|
||||
subText = APPROVAL_MODE_TEXT.HINT_SWITCH_TO_AUTO_EDIT_MODE;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ export const DialogManager = ({
|
||||
|
||||
const uiState = useUIState();
|
||||
const uiActions = useUIActions();
|
||||
|
||||
const {
|
||||
constrainHeight,
|
||||
terminalHeight,
|
||||
|
||||
@@ -71,8 +71,7 @@ export const Footer: React.FC = () => {
|
||||
const justifyContent = hideCWD && hideModelInfo ? 'center' : 'space-between';
|
||||
const displayVimMode = vimEnabled ? vimMode : undefined;
|
||||
|
||||
const showDebugProfiler =
|
||||
debugMode || (isDevelopment && settings.merged.general.devtools);
|
||||
const showDebugProfiler = debugMode || isDevelopment;
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -96,7 +96,6 @@ describe('<Header />', () => {
|
||||
},
|
||||
background: {
|
||||
primary: '',
|
||||
hintMode: '',
|
||||
diff: { added: '', removed: '' },
|
||||
},
|
||||
border: {
|
||||
|
||||
@@ -44,18 +44,6 @@ describe('<HistoryItemDisplay />', () => {
|
||||
expect(lastFrame()).toContain('Hello');
|
||||
});
|
||||
|
||||
it('renders HintMessage for "hint" type', () => {
|
||||
const item: HistoryItem = {
|
||||
...baseItem,
|
||||
type: 'hint',
|
||||
text: 'Try using ripgrep first',
|
||||
};
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
);
|
||||
expect(lastFrame()).toContain('Try using ripgrep first');
|
||||
});
|
||||
|
||||
it('renders UserMessage for "user" type with slash command', () => {
|
||||
const item: HistoryItem = {
|
||||
...baseItem,
|
||||
|
||||
@@ -35,7 +35,6 @@ import { ChatList } from './views/ChatList.js';
|
||||
import { HooksList } from './views/HooksList.js';
|
||||
import { ModelMessage } from './messages/ModelMessage.js';
|
||||
import { ThinkingMessage } from './messages/ThinkingMessage.js';
|
||||
import { HintMessage } from './messages/HintMessage.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
@@ -72,9 +71,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
{itemForDisplay.type === 'thinking' && inlineThinkingMode !== 'off' && (
|
||||
<ThinkingMessage thought={itemForDisplay.thought} />
|
||||
)}
|
||||
{itemForDisplay.type === 'hint' && (
|
||||
<HintMessage text={itemForDisplay.text} />
|
||||
)}
|
||||
{itemForDisplay.type === 'user' && (
|
||||
<UserMessage text={itemForDisplay.text} width={terminalWidth} />
|
||||
)}
|
||||
@@ -106,7 +102,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
text={itemForDisplay.text}
|
||||
icon={itemForDisplay.icon}
|
||||
color={itemForDisplay.color}
|
||||
marginBottom={itemForDisplay.marginBottom}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'warning' && (
|
||||
|
||||
@@ -281,7 +281,10 @@ describe('InputPrompt', () => {
|
||||
navigateDown: vi.fn(),
|
||||
handleSubmit: vi.fn(),
|
||||
};
|
||||
mockedUseInputHistory.mockReturnValue(mockInputHistory);
|
||||
mockedUseInputHistory.mockImplementation(({ onSubmit }) => {
|
||||
mockInputHistory.handleSubmit = vi.fn((val) => onSubmit(val));
|
||||
return mockInputHistory;
|
||||
});
|
||||
|
||||
mockReverseSearchCompletion = {
|
||||
suggestions: [],
|
||||
@@ -4093,7 +4096,7 @@ describe('InputPrompt', () => {
|
||||
beforeEach(() => {
|
||||
props.userMessages = ['first message', 'second message'];
|
||||
// Mock useInputHistory to actually call onChange
|
||||
mockedUseInputHistory.mockImplementation(({ onChange }) => ({
|
||||
mockedUseInputHistory.mockImplementation(({ onChange, onSubmit }) => ({
|
||||
navigateUp: () => {
|
||||
onChange('second message', 'start');
|
||||
return true;
|
||||
@@ -4102,7 +4105,7 @@ describe('InputPrompt', () => {
|
||||
onChange('first message', 'end');
|
||||
return true;
|
||||
},
|
||||
handleSubmit: vi.fn(),
|
||||
handleSubmit: vi.fn((val) => onSubmit(val)),
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
]);
|
||||
const [expandedSuggestionIndex, setExpandedSuggestionIndex] =
|
||||
useState<number>(-1);
|
||||
const shellHistory = useShellHistory(config.getProjectRoot(), config.storage);
|
||||
const shellHistory = useShellHistory(config.getProjectRoot());
|
||||
const shellHistoryData = shellHistory.history;
|
||||
|
||||
const completion = useCommandCompletion({
|
||||
@@ -337,31 +337,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(submittedValue: string) => {
|
||||
const trimmedMessage = submittedValue.trim();
|
||||
const isSlash = isSlashCommand(trimmedMessage);
|
||||
|
||||
const isShell = shellModeActive;
|
||||
if (
|
||||
(isSlash || isShell) &&
|
||||
streamingState === StreamingState.Responding
|
||||
) {
|
||||
setQueueErrorMessage(
|
||||
`${isShell ? 'Shell' : 'Slash'} commands cannot be queued`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
handleSubmitAndClear(trimmedMessage);
|
||||
},
|
||||
[
|
||||
handleSubmitAndClear,
|
||||
shellModeActive,
|
||||
streamingState,
|
||||
setQueueErrorMessage,
|
||||
],
|
||||
);
|
||||
|
||||
const customSetTextAndResetCompletionSignal = useCallback(
|
||||
(newText: string, cursorPosition?: 'start' | 'end' | number) => {
|
||||
buffer.setText(newText, cursorPosition);
|
||||
@@ -381,6 +356,26 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
onChange: customSetTextAndResetCompletionSignal,
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(submittedValue: string) => {
|
||||
const trimmedMessage = submittedValue.trim();
|
||||
const isSlash = isSlashCommand(trimmedMessage);
|
||||
|
||||
const isShell = shellModeActive;
|
||||
if (
|
||||
(isSlash || isShell) &&
|
||||
streamingState === StreamingState.Responding
|
||||
) {
|
||||
setQueueErrorMessage(
|
||||
`${isShell ? 'Shell' : 'Slash'} commands cannot be queued`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
inputHistory.handleSubmit(trimmedMessage);
|
||||
},
|
||||
[inputHistory, shellModeActive, streamingState, setQueueErrorMessage],
|
||||
);
|
||||
|
||||
// Effect to reset completion if history navigation just occurred and set the text
|
||||
useEffect(() => {
|
||||
if (suppressCompletion) {
|
||||
@@ -858,7 +853,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
showSuggestions && activeSuggestionIndex > -1
|
||||
? suggestions[activeSuggestionIndex].value
|
||||
: buffer.text;
|
||||
handleSubmitAndClear(textToSubmit);
|
||||
handleSubmit(textToSubmit);
|
||||
resetState();
|
||||
setActive(false);
|
||||
return true;
|
||||
@@ -1155,7 +1150,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
setShellModeActive,
|
||||
onClearScreen,
|
||||
inputHistory,
|
||||
handleSubmitAndClear,
|
||||
handleSubmit,
|
||||
shellHistory,
|
||||
reverseSearchCompletion,
|
||||
|
||||
@@ -14,8 +14,14 @@ import {
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
AuthType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
|
||||
// Mock dependencies
|
||||
const mockGetDisplayString = vi.fn();
|
||||
@@ -42,12 +48,14 @@ describe('<ModelDialog />', () => {
|
||||
const mockGetModel = vi.fn();
|
||||
const mockOnClose = vi.fn();
|
||||
const mockGetHasAccessToPreviewModel = vi.fn();
|
||||
const mockGetGemini31LaunchedSync = vi.fn();
|
||||
|
||||
interface MockConfig extends Partial<Config> {
|
||||
setModel: (model: string, isTemporary?: boolean) => void;
|
||||
getModel: () => string;
|
||||
getHasAccessToPreviewModel: () => boolean;
|
||||
getIdeMode: () => boolean;
|
||||
getGemini31LaunchedSync: () => boolean;
|
||||
}
|
||||
|
||||
const mockConfig: MockConfig = {
|
||||
@@ -55,12 +63,14 @@ describe('<ModelDialog />', () => {
|
||||
getModel: mockGetModel,
|
||||
getHasAccessToPreviewModel: mockGetHasAccessToPreviewModel,
|
||||
getIdeMode: () => false,
|
||||
getGemini31LaunchedSync: mockGetGemini31LaunchedSync,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(false);
|
||||
mockGetGemini31LaunchedSync.mockReturnValue(false);
|
||||
|
||||
// Default implementation for getDisplayString
|
||||
mockGetDisplayString.mockImplementation((val: string) => {
|
||||
@@ -70,11 +80,24 @@ describe('<ModelDialog />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const renderComponent = (configValue = mockConfig as Config) =>
|
||||
renderWithProviders(<ModelDialog onClose={mockOnClose} />, {
|
||||
config: configValue,
|
||||
const renderComponent = (
|
||||
configValue = mockConfig as Config,
|
||||
authType = AuthType.LOGIN_WITH_GOOGLE,
|
||||
) => {
|
||||
const settings = createMockSettings({
|
||||
security: {
|
||||
auth: {
|
||||
selectedType: authType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return renderWithProviders(<ModelDialog onClose={mockOnClose} />, {
|
||||
config: configValue,
|
||||
settings,
|
||||
});
|
||||
};
|
||||
|
||||
it('renders the initial "main" view correctly', () => {
|
||||
const { lastFrame } = renderComponent();
|
||||
expect(lastFrame()).toContain('Select Model');
|
||||
@@ -210,4 +233,96 @@ describe('<ModelDialog />', () => {
|
||||
expect(lastFrame()).toContain('Manual');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the preferred manual model in the main view option', () => {
|
||||
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL);
|
||||
const { lastFrame, unmount } = renderComponent();
|
||||
|
||||
expect(lastFrame()).toContain(`Manual (${DEFAULT_GEMINI_MODEL})`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('Preview Models', () => {
|
||||
beforeEach(() => {
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('shows Auto (Preview) in main view when access is granted', () => {
|
||||
const { lastFrame, unmount } = renderComponent();
|
||||
expect(lastFrame()).toContain('Auto (Preview)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows Gemini 3 models in manual view when Gemini 3.1 is NOT launched', async () => {
|
||||
mockGetGemini31LaunchedSync.mockReturnValue(false);
|
||||
const { lastFrame, stdin, unmount } = renderComponent();
|
||||
|
||||
// Go to manual view
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Manual
|
||||
});
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(PREVIEW_GEMINI_MODEL);
|
||||
expect(output).toContain(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows Gemini 3.1 models in manual view when Gemini 3.1 IS launched', async () => {
|
||||
mockGetGemini31LaunchedSync.mockReturnValue(true);
|
||||
const { lastFrame, stdin, unmount } = renderComponent(
|
||||
mockConfig as Config,
|
||||
AuthType.USE_VERTEX_AI,
|
||||
);
|
||||
|
||||
// Go to manual view
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Manual
|
||||
});
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(PREVIEW_GEMINI_3_1_MODEL);
|
||||
expect(output).toContain(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('uses custom tools model when Gemini 3.1 IS launched and auth is Gemini API Key', async () => {
|
||||
mockGetGemini31LaunchedSync.mockReturnValue(true);
|
||||
const { stdin, unmount } = renderComponent(
|
||||
mockConfig as Config,
|
||||
AuthType.USE_GEMINI,
|
||||
);
|
||||
|
||||
// Go to manual view
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Manual
|
||||
});
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
|
||||
// Select Gemini 3.1 (first item in preview section)
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetModel).toHaveBeenCalledWith(
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
true,
|
||||
);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import {
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
@@ -18,11 +19,14 @@ import {
|
||||
ModelSlashCommandEvent,
|
||||
logModelSlashCommand,
|
||||
getDisplayString,
|
||||
AuthType,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
interface ModelDialogProps {
|
||||
onClose: () => void;
|
||||
@@ -30,6 +34,7 @@ interface ModelDialogProps {
|
||||
|
||||
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
const config = useContext(ConfigContext);
|
||||
const settings = useSettings();
|
||||
const [view, setView] = useState<'main' | 'manual'>('main');
|
||||
const [persistMode, setPersistMode] = useState(false);
|
||||
|
||||
@@ -37,6 +42,10 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
|
||||
|
||||
const shouldShowPreviewModels = config?.getHasAccessToPreviewModel();
|
||||
const useGemini31 = config?.getGemini31LaunchedSync?.() ?? false;
|
||||
const selectedAuthType = settings.merged.security.auth.selectedType;
|
||||
const useCustomToolModel =
|
||||
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
|
||||
|
||||
const manualModelSelected = useMemo(() => {
|
||||
const manualModels = [
|
||||
@@ -44,6 +53,8 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
];
|
||||
if (manualModels.includes(preferredModel)) {
|
||||
@@ -94,13 +105,14 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
list.unshift({
|
||||
value: PREVIEW_GEMINI_MODEL_AUTO,
|
||||
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
|
||||
description:
|
||||
'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
|
||||
description: useGemini31
|
||||
? 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash'
|
||||
: 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
|
||||
key: PREVIEW_GEMINI_MODEL_AUTO,
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}, [shouldShowPreviewModels, manualModelSelected]);
|
||||
}, [shouldShowPreviewModels, manualModelSelected, useGemini31]);
|
||||
|
||||
const manualOptions = useMemo(() => {
|
||||
const list = [
|
||||
@@ -122,11 +134,19 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
];
|
||||
|
||||
if (shouldShowPreviewModels) {
|
||||
const previewProModel = useGemini31
|
||||
? PREVIEW_GEMINI_3_1_MODEL
|
||||
: PREVIEW_GEMINI_MODEL;
|
||||
|
||||
const previewProValue = useCustomToolModel
|
||||
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
|
||||
: previewProModel;
|
||||
|
||||
list.unshift(
|
||||
{
|
||||
value: PREVIEW_GEMINI_MODEL,
|
||||
title: PREVIEW_GEMINI_MODEL,
|
||||
key: PREVIEW_GEMINI_MODEL,
|
||||
value: previewProValue,
|
||||
title: previewProModel,
|
||||
key: previewProModel,
|
||||
},
|
||||
{
|
||||
value: PREVIEW_GEMINI_FLASH_MODEL,
|
||||
@@ -136,7 +156,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}, [shouldShowPreviewModels]);
|
||||
}, [shouldShowPreviewModels, useGemini31, useCustomToolModel]);
|
||||
|
||||
const options = view === 'main' ? mainOptions : manualOptions;
|
||||
|
||||
|
||||
@@ -23,11 +23,13 @@ import {
|
||||
import { computeSessionStats } from '../utils/computeStats.js';
|
||||
import {
|
||||
type RetrieveUserQuotaResponse,
|
||||
VALID_GEMINI_MODELS,
|
||||
isActiveModel,
|
||||
getDisplayString,
|
||||
isAutoModel,
|
||||
AuthType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import type { QuotaStats } from '../types.js';
|
||||
import { QuotaStatsInfo } from './QuotaStatsInfo.js';
|
||||
|
||||
@@ -82,9 +84,13 @@ const Section: React.FC<SectionProps> = ({ title, children }) => (
|
||||
const buildModelRows = (
|
||||
models: Record<string, ModelMetrics>,
|
||||
quotas?: RetrieveUserQuotaResponse,
|
||||
useGemini3_1 = false,
|
||||
useCustomToolModel = false,
|
||||
) => {
|
||||
const getBaseModelName = (name: string) => name.replace('-001', '');
|
||||
const usedModelNames = new Set(Object.keys(models).map(getBaseModelName));
|
||||
const usedModelNames = new Set(
|
||||
Object.keys(models).map(getBaseModelName).map(getDisplayString),
|
||||
);
|
||||
|
||||
// 1. Models with active usage
|
||||
const activeRows = Object.entries(models).map(([name, metrics]) => {
|
||||
@@ -93,7 +99,7 @@ const buildModelRows = (
|
||||
const inputTokens = metrics.tokens.input;
|
||||
return {
|
||||
key: name,
|
||||
modelName,
|
||||
modelName: getDisplayString(modelName),
|
||||
requests: metrics.api.totalRequests,
|
||||
cachedTokens: cachedTokens.toLocaleString(),
|
||||
inputTokens: inputTokens.toLocaleString(),
|
||||
@@ -109,12 +115,12 @@ const buildModelRows = (
|
||||
?.filter(
|
||||
(b) =>
|
||||
b.modelId &&
|
||||
VALID_GEMINI_MODELS.has(b.modelId) &&
|
||||
!usedModelNames.has(b.modelId),
|
||||
isActiveModel(b.modelId, useGemini3_1, useCustomToolModel) &&
|
||||
!usedModelNames.has(getDisplayString(b.modelId)),
|
||||
)
|
||||
.map((bucket) => ({
|
||||
key: bucket.modelId!,
|
||||
modelName: bucket.modelId!,
|
||||
modelName: getDisplayString(bucket.modelId!),
|
||||
requests: '-',
|
||||
cachedTokens: '-',
|
||||
inputTokens: '-',
|
||||
@@ -135,6 +141,8 @@ const ModelUsageTable: React.FC<{
|
||||
pooledRemaining?: number;
|
||||
pooledLimit?: number;
|
||||
pooledResetTime?: string;
|
||||
useGemini3_1?: boolean;
|
||||
useCustomToolModel?: boolean;
|
||||
}> = ({
|
||||
models,
|
||||
quotas,
|
||||
@@ -144,8 +152,10 @@ const ModelUsageTable: React.FC<{
|
||||
pooledRemaining,
|
||||
pooledLimit,
|
||||
pooledResetTime,
|
||||
useGemini3_1,
|
||||
useCustomToolModel,
|
||||
}) => {
|
||||
const rows = buildModelRows(models, quotas);
|
||||
const rows = buildModelRows(models, quotas, useGemini3_1, useCustomToolModel);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
@@ -401,7 +411,11 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
const { models, tools, files } = metrics;
|
||||
const computed = computeSessionStats(metrics);
|
||||
const settings = useSettings();
|
||||
|
||||
const config = useConfig();
|
||||
const useGemini3_1 = config.getGemini31LaunchedSync?.() ?? false;
|
||||
const useCustomToolModel =
|
||||
useGemini3_1 &&
|
||||
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI;
|
||||
const pooledRemaining = quotaStats?.remaining;
|
||||
const pooledLimit = quotaStats?.limit;
|
||||
const pooledResetTime = quotaStats?.resetTime;
|
||||
@@ -535,6 +549,8 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
pooledRemaining={pooledRemaining}
|
||||
pooledLimit={pooledLimit}
|
||||
pooledResetTime={pooledResetTime}
|
||||
useGemini3_1={useGemini3_1}
|
||||
useCustomToolModel={useCustomToolModel}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ApprovalModeIndicator > renders correctly for AUTO_EDIT mode 1`] = `"auto-accept edits shift+tab to manual"`;
|
||||
|
||||
exports[`ApprovalModeIndicator > renders correctly for AUTO_EDIT mode with plan enabled 1`] = `"auto-accept edits shift+tab to plan"`;
|
||||
|
||||
exports[`ApprovalModeIndicator > renders correctly for DEFAULT mode 1`] = `"shift+tab to accept edits"`;
|
||||
|
||||
exports[`ApprovalModeIndicator > renders correctly for DEFAULT mode with plan enabled 1`] = `"shift+tab to accept edits"`;
|
||||
|
||||
exports[`ApprovalModeIndicator > renders correctly for PLAN mode 1`] = `"plan shift+tab to manual"`;
|
||||
|
||||
exports[`ApprovalModeIndicator > renders correctly for YOLO mode 1`] = `"YOLO ctrl+y"`;
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SCREEN_READER_USER_PREFIX } from '../../textConstants.js';
|
||||
import { HalfLinePaddedBox } from '../shared/HalfLinePaddedBox.js';
|
||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||
|
||||
interface HintMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const HintMessage: React.FC<HintMessageProps> = ({ text }) => {
|
||||
const prefix = '💡 ';
|
||||
const prefixWidth = prefix.length;
|
||||
const config = useConfig();
|
||||
const useBackgroundColor = config.getUseBackgroundColor();
|
||||
|
||||
return (
|
||||
<HalfLinePaddedBox
|
||||
backgroundBaseColor={theme.text.accent}
|
||||
backgroundOpacity={0.1}
|
||||
useBackgroundColor={useBackgroundColor}
|
||||
>
|
||||
<Box
|
||||
flexDirection="row"
|
||||
paddingY={0}
|
||||
marginY={useBackgroundColor ? 0 : 1}
|
||||
paddingX={useBackgroundColor ? 1 : 0}
|
||||
alignSelf="flex-start"
|
||||
>
|
||||
<Box width={prefixWidth} flexShrink={0}>
|
||||
<Text
|
||||
color={theme.text.accent}
|
||||
aria-label={SCREEN_READER_USER_PREFIX}
|
||||
>
|
||||
{prefix}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="wrap" italic color={theme.text.accent}>
|
||||
{`Steering Hint: ${text}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</HalfLinePaddedBox>
|
||||
);
|
||||
};
|
||||
@@ -13,21 +13,19 @@ interface InfoMessageProps {
|
||||
text: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
marginBottom?: number;
|
||||
}
|
||||
|
||||
export const InfoMessage: React.FC<InfoMessageProps> = ({
|
||||
text,
|
||||
icon,
|
||||
color,
|
||||
marginBottom,
|
||||
}) => {
|
||||
color ??= theme.status.warning;
|
||||
const prefix = icon ?? 'ℹ ';
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1} marginBottom={marginBottom ?? 0}>
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={color}>{prefix}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type React from 'react';
|
||||
import { Text, Box } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { getDisplayString } from '@google/gemini-cli-core';
|
||||
|
||||
interface ModelMessageProps {
|
||||
model: string;
|
||||
@@ -15,7 +16,7 @@ interface ModelMessageProps {
|
||||
export const ModelMessage: React.FC<ModelMessageProps> = ({ model }) => (
|
||||
<Box marginLeft={2}>
|
||||
<Text color={theme.ui.comment} italic>
|
||||
Responding with {model}
|
||||
Responding with {getDisplayString(model)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -73,10 +73,6 @@ export interface UIActions {
|
||||
setActiveBackgroundShellPid: (pid: number) => void;
|
||||
setIsBackgroundShellListOpen: (isOpen: boolean) => void;
|
||||
setAuthContext: (context: { requiresRestart?: boolean }) => void;
|
||||
onHintInput: (char: string) => void;
|
||||
onHintBackspace: () => void;
|
||||
onHintClear: () => void;
|
||||
onHintSubmit: (hint: string) => void;
|
||||
handleRestart: () => void;
|
||||
handleNewAgentsSelect: (choice: NewAgentsChoice) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -173,8 +173,6 @@ export interface UIState {
|
||||
isBackgroundShellListOpen: boolean;
|
||||
adminSettingsChanged: boolean;
|
||||
newAgents: AgentDefinition[] | null;
|
||||
hintMode: boolean;
|
||||
hintBuffer: string;
|
||||
transientMessage: {
|
||||
text: string;
|
||||
type: TransientMessageType;
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('useApprovalModeIndicator', () => {
|
||||
);
|
||||
expect(result.current).toBe(ApprovalMode.YOLO);
|
||||
|
||||
// Shift+Tab cycles back to DEFAULT (since PLAN is disabled by default in mock)
|
||||
// Shift+Tab cycles back to AUTO_EDIT (from YOLO)
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({
|
||||
name: 'tab',
|
||||
@@ -236,7 +236,7 @@ describe('useApprovalModeIndicator', () => {
|
||||
expect(result.current).toBe(ApprovalMode.AUTO_EDIT);
|
||||
});
|
||||
|
||||
it('should cycle through DEFAULT -> PLAN -> AUTO_EDIT -> DEFAULT when plan is enabled', () => {
|
||||
it('should cycle through DEFAULT -> AUTO_EDIT -> PLAN -> DEFAULT when plan is enabled', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
mockConfigInstance.isPlanEnabled.mockReturnValue(true);
|
||||
renderHook(() =>
|
||||
@@ -246,15 +246,7 @@ describe('useApprovalModeIndicator', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// DEFAULT -> PLAN
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
|
||||
// PLAN -> AUTO_EDIT
|
||||
// DEFAULT -> AUTO_EDIT
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
@@ -262,7 +254,15 @@ describe('useApprovalModeIndicator', () => {
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
|
||||
// AUTO_EDIT -> DEFAULT
|
||||
// AUTO_EDIT -> PLAN
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.PLAN,
|
||||
);
|
||||
|
||||
// PLAN -> DEFAULT
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
|
||||
@@ -72,14 +72,14 @@ export function useApprovalModeIndicator({
|
||||
const currentMode = config.getApprovalMode();
|
||||
switch (currentMode) {
|
||||
case ApprovalMode.DEFAULT:
|
||||
nextApprovalMode = config.isPlanEnabled()
|
||||
? ApprovalMode.PLAN
|
||||
: ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case ApprovalMode.PLAN:
|
||||
nextApprovalMode = ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
nextApprovalMode = config.isPlanEnabled()
|
||||
? ApprovalMode.PLAN
|
||||
: ApprovalMode.DEFAULT;
|
||||
break;
|
||||
case ApprovalMode.PLAN:
|
||||
nextApprovalMode = ApprovalMode.DEFAULT;
|
||||
break;
|
||||
case ApprovalMode.YOLO:
|
||||
|
||||
@@ -56,11 +56,6 @@ const MockedGeminiClientClass = vi.hoisted(() =>
|
||||
this.startChat = mockStartChat;
|
||||
this.sendMessageStream = mockSendMessageStream;
|
||||
this.addHistory = vi.fn();
|
||||
this.generateContent = vi.fn().mockResolvedValue({
|
||||
candidates: [
|
||||
{ content: { parts: [{ text: 'Got it. Focusing on tests only.' }] } },
|
||||
],
|
||||
});
|
||||
this.getCurrentSequenceModel = vi.fn().mockReturnValue('test-model');
|
||||
this.getChat = vi.fn().mockReturnValue({
|
||||
recordCompletedToolCalls: vi.fn(),
|
||||
@@ -157,17 +152,13 @@ vi.mock('./useLogger.js', () => ({
|
||||
|
||||
const mockStartNewPrompt = vi.fn();
|
||||
const mockAddUsage = vi.fn();
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as any;
|
||||
return {
|
||||
...actual,
|
||||
useSessionStats: vi.fn(() => ({
|
||||
startNewPrompt: mockStartNewPrompt,
|
||||
addUsage: mockAddUsage,
|
||||
getPromptCount: vi.fn(() => 5),
|
||||
})),
|
||||
};
|
||||
});
|
||||
vi.mock('../contexts/SessionContext.js', () => ({
|
||||
useSessionStats: vi.fn(() => ({
|
||||
startNewPrompt: mockStartNewPrompt,
|
||||
addUsage: mockAddUsage,
|
||||
getPromptCount: vi.fn(() => 5),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('./slashCommandProcessor.js', () => ({
|
||||
handleSlashCommand: vi.fn().mockReturnValue(false),
|
||||
@@ -670,113 +661,6 @@ describe('useGeminiStream', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should inject steering hint prompt for continuation', async () => {
|
||||
const toolCallResponseParts: Part[] = [{ text: 'tool final response' }];
|
||||
const completedToolCalls: TrackedToolCall[] = [
|
||||
{
|
||||
request: {
|
||||
callId: 'call1',
|
||||
name: 'tool1',
|
||||
args: {},
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-id-ack',
|
||||
},
|
||||
status: 'success',
|
||||
responseSubmittedToGemini: false,
|
||||
response: {
|
||||
callId: 'call1',
|
||||
responseParts: toolCallResponseParts,
|
||||
errorType: undefined,
|
||||
},
|
||||
tool: {
|
||||
displayName: 'MockTool',
|
||||
},
|
||||
invocation: {
|
||||
getDescription: () => `Mock description`,
|
||||
} as unknown as AnyToolInvocation,
|
||||
} as TrackedCompletedToolCall,
|
||||
];
|
||||
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Applied the requested adjustment.',
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
let capturedOnComplete:
|
||||
| ((completedTools: TrackedToolCall[]) => Promise<void>)
|
||||
| null = null;
|
||||
mockUseToolScheduler.mockImplementation((onComplete) => {
|
||||
capturedOnComplete = onComplete;
|
||||
return [
|
||||
[],
|
||||
mockScheduleToolCalls,
|
||||
mockMarkToolsAsSubmitted,
|
||||
vi.fn(),
|
||||
mockCancelAllToolCalls,
|
||||
0,
|
||||
];
|
||||
});
|
||||
|
||||
renderHookWithProviders(() =>
|
||||
useGeminiStream(
|
||||
new MockedGeminiClientClass(mockConfig),
|
||||
[],
|
||||
mockAddItem,
|
||||
mockConfig,
|
||||
mockLoadedSettings,
|
||||
mockOnDebugMessage,
|
||||
mockHandleSlashCommand,
|
||||
false,
|
||||
() => 'vscode' as EditorType,
|
||||
() => {},
|
||||
() => Promise.resolve(),
|
||||
false,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
80,
|
||||
24,
|
||||
undefined,
|
||||
() => 'focus on tests only',
|
||||
),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
if (capturedOnComplete) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await capturedOnComplete(completedToolCalls);
|
||||
}
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const sentParts = mockSendMessageStream.mock.calls[0][0] as Part[];
|
||||
const injectedHintPart = sentParts[0] as { text?: string };
|
||||
expect(injectedHintPart.text).toContain(
|
||||
'User steering update: "focus on tests only"',
|
||||
);
|
||||
expect(injectedHintPart.text).toContain(
|
||||
'Classify it as ADD_TASK, MODIFY_TASK, CANCEL_TASK, or EXTRA_CONTEXT.',
|
||||
);
|
||||
expect(injectedHintPart.text).toContain(
|
||||
'Do not cancel/skip tasks unless the user explicitly cancels them.',
|
||||
);
|
||||
expect(
|
||||
mockAddItem.mock.calls.some(
|
||||
([item]) =>
|
||||
item?.type === 'info' &&
|
||||
typeof item.text === 'string' &&
|
||||
item.text.includes('Got it. Focusing on tests only.'),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle all tool calls being cancelled', async () => {
|
||||
const cancelledToolCalls: TrackedToolCall[] = [
|
||||
{
|
||||
|
||||
@@ -32,8 +32,6 @@ import {
|
||||
ValidationRequiredError,
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
buildUserSteeringHintPrompt,
|
||||
generateSteeringAckMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Config,
|
||||
@@ -83,7 +81,6 @@ import path from 'node:path';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import { useKeypress } from './useKeypress.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
type ToolResponseWithParts = ToolCallResponseInfo & {
|
||||
llmContent?: PartListUnion;
|
||||
@@ -188,7 +185,6 @@ export const useGeminiStream = (
|
||||
terminalWidth: number,
|
||||
terminalHeight: number,
|
||||
isShellFocused?: boolean,
|
||||
consumeUserHint?: () => string | null,
|
||||
) => {
|
||||
const [initError, setInitError] = useState<string | null>(null);
|
||||
const [retryStatus, setRetryStatus] = useState<RetryAttemptPayload | null>(
|
||||
@@ -1565,28 +1561,6 @@ export const useGeminiStream = (
|
||||
const responsesToSend: Part[] = geminiTools.flatMap(
|
||||
(toolCall) => toolCall.response.responseParts,
|
||||
);
|
||||
|
||||
if (consumeUserHint) {
|
||||
const userHint = consumeUserHint();
|
||||
if (userHint && userHint.trim().length > 0) {
|
||||
const hintText = userHint.trim();
|
||||
responsesToSend.unshift({
|
||||
text: buildUserSteeringHintPrompt(hintText),
|
||||
});
|
||||
void generateSteeringAckMessage(geminiClient, hintText).then(
|
||||
(ackText) => {
|
||||
addItem({
|
||||
type: 'info',
|
||||
icon: '· ',
|
||||
color: theme.text.secondary,
|
||||
marginBottom: 1,
|
||||
text: ackText,
|
||||
} as Omit<HistoryItem, 'id'>);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const callIdsToMarkAsSubmitted = geminiTools.map(
|
||||
(toolCall) => toolCall.request.callId,
|
||||
);
|
||||
@@ -1619,7 +1593,6 @@ export const useGeminiStream = (
|
||||
modelSwitchedFromQuotaError,
|
||||
addItem,
|
||||
registerBackgroundShell,
|
||||
consumeUserHint,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -155,9 +155,10 @@ describe('useQuotaAndFallback', () => {
|
||||
expect(request?.isTerminalQuotaError).toBe(true);
|
||||
|
||||
const message = request!.message;
|
||||
expect(message).toContain('Usage limit reached for gemini-pro.');
|
||||
expect(message).toContain('Usage limit reached for all Pro models.');
|
||||
expect(message).toContain('Access resets at'); // From getResetTimeMessage
|
||||
expect(message).toContain('/stats model for usage details');
|
||||
expect(message).toContain('/model to switch models.');
|
||||
expect(message).toContain('/auth to switch to API key.');
|
||||
|
||||
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
||||
@@ -176,6 +177,77 @@ describe('useQuotaAndFallback', () => {
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should show the model name for a terminal quota error on a non-pro model', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: UserTierId.FREE,
|
||||
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||
onShowAuthSelection: mockOnShowAuthSelection,
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
const error = new TerminalQuotaError(
|
||||
'flash quota',
|
||||
mockGoogleApiError,
|
||||
1000 * 60 * 5,
|
||||
);
|
||||
act(() => {
|
||||
promise = handler('gemini-flash', 'gemini-pro', error);
|
||||
});
|
||||
|
||||
const request = result.current.proQuotaRequest;
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.failedModel).toBe('gemini-flash');
|
||||
|
||||
const message = request!.message;
|
||||
expect(message).toContain('Usage limit reached for gemini-flash.');
|
||||
expect(message).not.toContain('all Pro models');
|
||||
|
||||
act(() => {
|
||||
result.current.handleProQuotaChoice('retry_later');
|
||||
});
|
||||
|
||||
await promise!;
|
||||
});
|
||||
|
||||
it('should handle terminal quota error without retry delay', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: UserTierId.FREE,
|
||||
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||
onShowAuthSelection: mockOnShowAuthSelection,
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
const error = new TerminalQuotaError('no delay', mockGoogleApiError);
|
||||
act(() => {
|
||||
promise = handler('gemini-pro', 'gemini-flash', error);
|
||||
});
|
||||
|
||||
const request = result.current.proQuotaRequest;
|
||||
const message = request!.message;
|
||||
expect(message).not.toContain('Access resets at');
|
||||
expect(message).toContain('Usage limit reached for all Pro models.');
|
||||
|
||||
act(() => {
|
||||
result.current.handleProQuotaChoice('retry_later');
|
||||
});
|
||||
|
||||
await promise!;
|
||||
});
|
||||
|
||||
it('should handle race conditions by stopping subsequent requests', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
TerminalQuotaError,
|
||||
ModelNotFoundError,
|
||||
type UserTierId,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
VALID_GEMINI_MODELS,
|
||||
isProModel,
|
||||
getDisplayString,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -67,11 +67,9 @@ export function useQuotaAndFallback({
|
||||
let message: string;
|
||||
let isTerminalQuotaError = false;
|
||||
let isModelNotFoundError = false;
|
||||
const usageLimitReachedModel =
|
||||
failedModel === DEFAULT_GEMINI_MODEL ||
|
||||
failedModel === PREVIEW_GEMINI_MODEL
|
||||
? 'all Pro models'
|
||||
: failedModel;
|
||||
const usageLimitReachedModel = isProModel(failedModel)
|
||||
? 'all Pro models'
|
||||
: failedModel;
|
||||
if (error instanceof TerminalQuotaError) {
|
||||
isTerminalQuotaError = true;
|
||||
// Common part of the message for both tiers
|
||||
@@ -89,7 +87,7 @@ export function useQuotaAndFallback({
|
||||
) {
|
||||
isModelNotFoundError = true;
|
||||
const messageLines = [
|
||||
`It seems like you don't have access to ${failedModel}.`,
|
||||
`It seems like you don't have access to ${getDisplayString(failedModel)}.`,
|
||||
`Your admin might have disabled the access. Contact them to enable the Preview Release Channel.`,
|
||||
];
|
||||
message = messageLines.join('\n');
|
||||
|
||||
@@ -79,26 +79,14 @@ export function useShellHistory(
|
||||
const [historyFilePath, setHistoryFilePath] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const filePath = await getHistoryFilePath(projectRoot, storage);
|
||||
if (!isMounted) return;
|
||||
setHistoryFilePath(filePath);
|
||||
const loadedHistory = await readHistoryFile(filePath);
|
||||
if (!isMounted) return;
|
||||
setHistory(loadedHistory.reverse()); // Newest first
|
||||
} catch (error) {
|
||||
if (isMounted) {
|
||||
debugLogger.error('Error loading shell history:', error);
|
||||
}
|
||||
}
|
||||
const filePath = await getHistoryFilePath(projectRoot, storage);
|
||||
setHistoryFilePath(filePath);
|
||||
const loadedHistory = await readHistoryFile(filePath);
|
||||
setHistory(loadedHistory.reverse()); // Newest first
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
loadHistory();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [projectRoot, storage]);
|
||||
|
||||
const addCommandToHistory = useCallback(
|
||||
|
||||
@@ -36,7 +36,6 @@ const noColorSemanticColors: SemanticColors = {
|
||||
},
|
||||
background: {
|
||||
primary: '',
|
||||
hintMode: '',
|
||||
diff: {
|
||||
added: '',
|
||||
removed: '',
|
||||
|
||||
@@ -16,7 +16,6 @@ export interface SemanticColors {
|
||||
};
|
||||
background: {
|
||||
primary: string;
|
||||
hintMode: string;
|
||||
diff: {
|
||||
added: string;
|
||||
removed: string;
|
||||
@@ -49,7 +48,6 @@ export const lightSemanticColors: SemanticColors = {
|
||||
},
|
||||
background: {
|
||||
primary: lightTheme.Background,
|
||||
hintMode: '#E8E0F0',
|
||||
diff: {
|
||||
added: lightTheme.DiffAdded,
|
||||
removed: lightTheme.DiffRemoved,
|
||||
@@ -82,7 +80,6 @@ export const darkSemanticColors: SemanticColors = {
|
||||
},
|
||||
background: {
|
||||
primary: darkTheme.Background,
|
||||
hintMode: '#352A45',
|
||||
diff: {
|
||||
added: darkTheme.DiffAdded,
|
||||
removed: darkTheme.DiffRemoved,
|
||||
|
||||
@@ -131,7 +131,6 @@ export class Theme {
|
||||
},
|
||||
background: {
|
||||
primary: this.colors.Background,
|
||||
hintMode: this.type === 'light' ? '#E8E0F0' : '#352A45',
|
||||
diff: {
|
||||
added: this.colors.DiffAdded,
|
||||
removed: this.colors.DiffRemoved,
|
||||
@@ -401,7 +400,6 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
|
||||
},
|
||||
background: {
|
||||
primary: customTheme.background?.primary ?? colors.Background,
|
||||
hintMode: 'magenta',
|
||||
diff: {
|
||||
added: customTheme.background?.diff?.added ?? colors.DiffAdded,
|
||||
removed: customTheme.background?.diff?.removed ?? colors.DiffRemoved,
|
||||
|
||||
@@ -123,7 +123,6 @@ export type HistoryItemInfo = HistoryItemBase & {
|
||||
text: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
marginBottom?: number;
|
||||
};
|
||||
|
||||
export type HistoryItemError = HistoryItemBase & {
|
||||
@@ -226,11 +225,6 @@ export type HistoryItemThinking = HistoryItemBase & {
|
||||
thought: ThoughtSummary;
|
||||
};
|
||||
|
||||
export type HistoryItemHint = HistoryItemBase & {
|
||||
type: 'hint';
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type HistoryItemChatList = HistoryItemBase & {
|
||||
type: 'chat_list';
|
||||
chats: ChatDetail[];
|
||||
@@ -355,7 +349,6 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemMcpStatus
|
||||
| HistoryItemChatList
|
||||
| HistoryItemThinking
|
||||
| HistoryItemHint
|
||||
| HistoryItemHooksList;
|
||||
|
||||
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
||||
@@ -381,7 +374,6 @@ export enum MessageType {
|
||||
MCP_STATUS = 'mcp_status',
|
||||
CHAT_LIST = 'chat_list',
|
||||
HOOKS_LIST = 'hooks_list',
|
||||
HINT = 'hint',
|
||||
}
|
||||
|
||||
// Simplified message structure for internal feedback
|
||||
|
||||
@@ -483,7 +483,10 @@ export class Session {
|
||||
const functionCalls: FunctionCall[] = [];
|
||||
|
||||
try {
|
||||
const model = resolveModel(this.config.getModel());
|
||||
const model = resolveModel(
|
||||
this.config.getModel(),
|
||||
(await this.config.getGemini31Launched?.()) ?? false,
|
||||
);
|
||||
const responseStream = await chat.sendMessageStream(
|
||||
{ model },
|
||||
nextMessage?.parts ?? [],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.29.0-nightly.20260203.71f46f116",
|
||||
"version": "0.29.5",
|
||||
"description": "Gemini CLI Core",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -59,7 +59,6 @@ import { getVersion } from '../utils/version.js';
|
||||
import { getToolCallContext } from '../utils/toolCallContext.js';
|
||||
import { scheduleAgentTools } from './agent-scheduler.js';
|
||||
import { DeadlineTimer } from '../utils/deadlineTimer.js';
|
||||
import { formatUserHintsForModel } from '../utils/flashLiteHelper.js';
|
||||
|
||||
/** A callback function to report on agent activity. */
|
||||
export type ActivityCallback = (activity: SubagentActivityEvent) => void;
|
||||
@@ -463,17 +462,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
const query = this.definition.promptConfig.query
|
||||
? templateString(this.definition.promptConfig.query, augmentedInputs)
|
||||
: DEFAULT_QUERY_STRING;
|
||||
|
||||
let lastProcessedHintIndex = this.runtimeContext.getLatestHintIndex();
|
||||
const initialHints = this.runtimeContext.getUserHints();
|
||||
const formattedInitialHints = formatUserHintsForModel(initialHints);
|
||||
|
||||
let currentMessage: Content = formattedInitialHints
|
||||
? {
|
||||
role: 'user',
|
||||
parts: [{ text: formattedInitialHints }, { text: query }],
|
||||
}
|
||||
: { role: 'user', parts: [{ text: query }] };
|
||||
let currentMessage: Content = { role: 'user', parts: [{ text: query }] };
|
||||
|
||||
while (true) {
|
||||
// Check for termination conditions like max turns.
|
||||
@@ -512,20 +501,6 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
|
||||
// If status is 'continue', update message for the next loop
|
||||
currentMessage = turnResult.nextMessage;
|
||||
|
||||
// Check for new user steering hints
|
||||
const newHints = this.runtimeContext.getUserHintsAfter(
|
||||
lastProcessedHintIndex,
|
||||
);
|
||||
if (newHints.length > 0) {
|
||||
const formattedHints = formatUserHintsForModel(newHints);
|
||||
if (formattedHints) {
|
||||
// Append hints to the current message (next turn)
|
||||
currentMessage.parts ??= [];
|
||||
currentMessage.parts.unshift({ text: formattedHints });
|
||||
}
|
||||
lastProcessedHintIndex = this.runtimeContext.getLatestHintIndex();
|
||||
}
|
||||
}
|
||||
|
||||
// === UNIFIED RECOVERY BLOCK ===
|
||||
|
||||
@@ -64,8 +64,6 @@ export class SubagentTool extends BaseDeclarativeTool<AgentInputs, ToolResult> {
|
||||
}
|
||||
}
|
||||
|
||||
import { formatUserHintsForModel } from '../utils/flashLiteHelper.js';
|
||||
|
||||
class SubAgentInvocation extends BaseToolInvocation<AgentInputs, ToolResult> {
|
||||
constructor(
|
||||
params: AgentInputs,
|
||||
@@ -90,10 +88,7 @@ class SubAgentInvocation extends BaseToolInvocation<AgentInputs, ToolResult> {
|
||||
override async shouldConfirmExecute(
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<ToolCallConfirmationDetails | false> {
|
||||
const invocation = this.buildSubInvocation(
|
||||
this.definition,
|
||||
this.withUserHints(this.params),
|
||||
);
|
||||
const invocation = this.buildSubInvocation(this.definition, this.params);
|
||||
return invocation.shouldConfirmExecute(abortSignal);
|
||||
}
|
||||
|
||||
@@ -112,36 +107,11 @@ class SubAgentInvocation extends BaseToolInvocation<AgentInputs, ToolResult> {
|
||||
);
|
||||
}
|
||||
|
||||
const invocation = this.buildSubInvocation(
|
||||
this.definition,
|
||||
this.withUserHints(this.params),
|
||||
);
|
||||
const invocation = this.buildSubInvocation(this.definition, this.params);
|
||||
|
||||
return invocation.execute(signal, updateOutput);
|
||||
}
|
||||
|
||||
private withUserHints(agentArgs: AgentInputs): AgentInputs {
|
||||
if (this.definition.kind !== 'remote') {
|
||||
return agentArgs;
|
||||
}
|
||||
|
||||
const userHints = this.config.getUserHints();
|
||||
const formattedHints = formatUserHintsForModel(userHints);
|
||||
if (!formattedHints) {
|
||||
return agentArgs;
|
||||
}
|
||||
|
||||
const query = agentArgs['query'];
|
||||
if (typeof query !== 'string' || query.trim().length === 0) {
|
||||
return agentArgs;
|
||||
}
|
||||
|
||||
return {
|
||||
...agentArgs,
|
||||
query: `${formattedHints}\n\n${query}`,
|
||||
};
|
||||
}
|
||||
|
||||
private buildSubInvocation(
|
||||
definition: AgentDefinition,
|
||||
agentArgs: AgentInputs,
|
||||
|
||||
@@ -43,7 +43,10 @@ export function resolvePolicyChain(
|
||||
const configuredModel = config.getModel();
|
||||
|
||||
let chain;
|
||||
const resolvedModel = resolveModel(modelFromConfig);
|
||||
const resolvedModel = resolveModel(
|
||||
modelFromConfig,
|
||||
config.getGemini31LaunchedSync?.() ?? false,
|
||||
);
|
||||
const isAutoPreferred = preferredModel ? isAutoModel(preferredModel) : false;
|
||||
const isAutoConfigured = isAutoModel(configuredModel);
|
||||
|
||||
|
||||
@@ -345,6 +345,7 @@ describe('Admin Controls', () => {
|
||||
// Should still start polling
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
strictModeDisabled: true,
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||
|
||||
@@ -363,7 +364,10 @@ describe('Admin Controls', () => {
|
||||
});
|
||||
|
||||
it('should fetch from server if no cachedSettings provided', async () => {
|
||||
const serverResponse = { strictModeDisabled: false };
|
||||
const serverResponse = {
|
||||
strictModeDisabled: false,
|
||||
adminControlsApplicable: true,
|
||||
};
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue(serverResponse);
|
||||
|
||||
const result = await fetchAdminControls(
|
||||
@@ -386,31 +390,24 @@ describe('Admin Controls', () => {
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return empty object on fetch error and still start polling', async () => {
|
||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(
|
||||
new Error('Network error'),
|
||||
);
|
||||
const result = await fetchAdminControls(
|
||||
mockServer,
|
||||
undefined,
|
||||
true,
|
||||
mockOnSettingsChanged,
|
||||
);
|
||||
it('should throw error on fetch error and NOT start polling', async () => {
|
||||
const error = new Error('Network error');
|
||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error);
|
||||
|
||||
expect(result).toEqual({});
|
||||
await expect(
|
||||
fetchAdminControls(mockServer, undefined, true, mockOnSettingsChanged),
|
||||
).rejects.toThrow(error);
|
||||
|
||||
// Polling should have been started and should retry
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
strictModeDisabled: false,
|
||||
});
|
||||
// Polling should NOT have been started
|
||||
// Advance timers just to be absolutely sure
|
||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(2); // Initial + poll
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1); // Only initial fetch
|
||||
});
|
||||
|
||||
it('should return empty object on 403 fetch error and STOP polling', async () => {
|
||||
const error403 = new Error('Forbidden');
|
||||
Object.assign(error403, { status: 403 });
|
||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error403);
|
||||
it('should return empty object on adminControlsApplicable false and STOP polling', async () => {
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
adminControlsApplicable: false,
|
||||
});
|
||||
|
||||
const result = await fetchAdminControls(
|
||||
mockServer,
|
||||
@@ -421,7 +418,7 @@ describe('Admin Controls', () => {
|
||||
|
||||
expect(result).toEqual({});
|
||||
|
||||
// Advance time - should NOT poll because of 403
|
||||
// Advance time - should NOT poll because of adminControlsApplicable: false
|
||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1); // Only the initial call
|
||||
});
|
||||
@@ -430,6 +427,7 @@ describe('Admin Controls', () => {
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
strictModeDisabled: false,
|
||||
unknownField: 'bad',
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
|
||||
const result = await fetchAdminControls(
|
||||
@@ -455,7 +453,9 @@ describe('Admin Controls', () => {
|
||||
});
|
||||
|
||||
it('should reset polling interval if called again', async () => {
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({});
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
|
||||
// First call
|
||||
await fetchAdminControls(
|
||||
@@ -514,6 +514,7 @@ describe('Admin Controls', () => {
|
||||
const serverResponse = {
|
||||
strictModeDisabled: true,
|
||||
unknownField: 'should be removed',
|
||||
adminControlsApplicable: true,
|
||||
};
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue(serverResponse);
|
||||
|
||||
@@ -532,22 +533,22 @@ describe('Admin Controls', () => {
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return empty object on 403 fetch error', async () => {
|
||||
const error403 = new Error('Forbidden');
|
||||
Object.assign(error403, { status: 403 });
|
||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error403);
|
||||
it('should return empty object on adminControlsApplicable false', async () => {
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
adminControlsApplicable: false,
|
||||
});
|
||||
|
||||
const result = await fetchAdminControlsOnce(mockServer, true);
|
||||
expect(result).toEqual({});
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return empty object on any other fetch error', async () => {
|
||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(
|
||||
new Error('Network error'),
|
||||
it('should throw error on any other fetch error', async () => {
|
||||
const error = new Error('Network error');
|
||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error);
|
||||
await expect(fetchAdminControlsOnce(mockServer, true)).rejects.toThrow(
|
||||
error,
|
||||
);
|
||||
const result = await fetchAdminControlsOnce(mockServer, true);
|
||||
expect(result).toEqual({});
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -555,7 +556,9 @@ describe('Admin Controls', () => {
|
||||
const setIntervalSpy = vi.spyOn(global, 'setInterval');
|
||||
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
||||
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({});
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
await fetchAdminControlsOnce(mockServer, true);
|
||||
|
||||
expect(setIntervalSpy).not.toHaveBeenCalled();
|
||||
@@ -568,6 +571,7 @@ describe('Admin Controls', () => {
|
||||
// Initial fetch
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
strictModeDisabled: true,
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
await fetchAdminControls(
|
||||
mockServer,
|
||||
@@ -579,6 +583,7 @@ describe('Admin Controls', () => {
|
||||
// Update for next poll
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
strictModeDisabled: false,
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
|
||||
// Fast forward
|
||||
@@ -598,7 +603,10 @@ describe('Admin Controls', () => {
|
||||
});
|
||||
|
||||
it('should NOT emit if settings are deeply equal but not the same instance', async () => {
|
||||
const settings = { strictModeDisabled: false };
|
||||
const settings = {
|
||||
strictModeDisabled: false,
|
||||
adminControlsApplicable: true,
|
||||
};
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue(settings);
|
||||
|
||||
await fetchAdminControls(
|
||||
@@ -613,6 +621,7 @@ describe('Admin Controls', () => {
|
||||
// Next poll returns a different object with the same values
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
strictModeDisabled: false,
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||
|
||||
@@ -623,6 +632,7 @@ describe('Admin Controls', () => {
|
||||
// Initial fetch is successful
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
strictModeDisabled: true,
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
await fetchAdminControls(
|
||||
mockServer,
|
||||
@@ -643,6 +653,7 @@ describe('Admin Controls', () => {
|
||||
// Subsequent poll succeeds with new data
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
strictModeDisabled: false,
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(3);
|
||||
@@ -659,10 +670,11 @@ describe('Admin Controls', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should STOP polling if server returns 403', async () => {
|
||||
it('should STOP polling if server returns adminControlsApplicable false', async () => {
|
||||
// Initial fetch is successful
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
strictModeDisabled: true,
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
await fetchAdminControls(
|
||||
mockServer,
|
||||
@@ -672,10 +684,10 @@ describe('Admin Controls', () => {
|
||||
);
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Next poll returns 403
|
||||
const error403 = new Error('Forbidden');
|
||||
Object.assign(error403, { status: 403 });
|
||||
(mockServer.fetchAdminControls as Mock).mockRejectedValue(error403);
|
||||
// Next poll returns adminControlsApplicable: false
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
adminControlsApplicable: false,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000);
|
||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(2);
|
||||
@@ -688,7 +700,9 @@ describe('Admin Controls', () => {
|
||||
|
||||
describe('stopAdminControlsPolling', () => {
|
||||
it('should stop polling after it has started', async () => {
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({});
|
||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||
adminControlsApplicable: true,
|
||||
});
|
||||
|
||||
// Start polling
|
||||
await fetchAdminControls(
|
||||
|
||||
@@ -80,15 +80,6 @@ export function sanitizeAdminSettings(
|
||||
};
|
||||
}
|
||||
|
||||
function isGaxiosError(error: unknown): error is { status: number } {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'status' in error &&
|
||||
typeof (error as { status: unknown }).status === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the admin controls from the server if enabled by experiment flag.
|
||||
* Safely handles polling start/stop based on the flag and server availability.
|
||||
@@ -113,7 +104,7 @@ export async function fetchAdminControls(
|
||||
|
||||
// If we already have settings (e.g. from IPC during relaunch), use them
|
||||
// to avoid blocking startup with another fetch. We'll still start polling.
|
||||
if (cachedSettings) {
|
||||
if (cachedSettings && Object.keys(cachedSettings).length !== 0) {
|
||||
currentSettings = cachedSettings;
|
||||
startAdminControlsPolling(server, server.projectId, onSettingsChanged);
|
||||
return cachedSettings;
|
||||
@@ -123,22 +114,20 @@ export async function fetchAdminControls(
|
||||
const rawSettings = await server.fetchAdminControls({
|
||||
project: server.projectId,
|
||||
});
|
||||
|
||||
if (rawSettings.adminControlsApplicable !== true) {
|
||||
stopAdminControlsPolling();
|
||||
currentSettings = undefined;
|
||||
return {};
|
||||
}
|
||||
|
||||
const sanitizedSettings = sanitizeAdminSettings(rawSettings);
|
||||
currentSettings = sanitizedSettings;
|
||||
startAdminControlsPolling(server, server.projectId, onSettingsChanged);
|
||||
return sanitizedSettings;
|
||||
} catch (e) {
|
||||
// Non-enterprise users don't have access to fetch settings.
|
||||
if (isGaxiosError(e) && e.status === 403) {
|
||||
stopAdminControlsPolling();
|
||||
currentSettings = undefined;
|
||||
return {};
|
||||
}
|
||||
debugLogger.error('Failed to fetch admin controls: ', e);
|
||||
// If initial fetch fails, start polling to retry.
|
||||
currentSettings = {};
|
||||
startAdminControlsPolling(server, server.projectId, onSettingsChanged);
|
||||
return {};
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,17 +151,18 @@ export async function fetchAdminControlsOnce(
|
||||
const rawSettings = await server.fetchAdminControls({
|
||||
project: server.projectId,
|
||||
});
|
||||
return sanitizeAdminSettings(rawSettings);
|
||||
} catch (e) {
|
||||
// Non-enterprise users don't have access to fetch settings.
|
||||
if (isGaxiosError(e) && e.status === 403) {
|
||||
|
||||
if (rawSettings.adminControlsApplicable !== true) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return sanitizeAdminSettings(rawSettings);
|
||||
} catch (e) {
|
||||
debugLogger.error(
|
||||
'Failed to fetch admin controls: ',
|
||||
e instanceof Error ? e.message : e,
|
||||
);
|
||||
return {};
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +182,13 @@ function startAdminControlsPolling(
|
||||
const rawSettings = await server.fetchAdminControls({
|
||||
project,
|
||||
});
|
||||
|
||||
if (rawSettings.adminControlsApplicable !== true) {
|
||||
stopAdminControlsPolling();
|
||||
currentSettings = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const newSettings = sanitizeAdminSettings(rawSettings);
|
||||
|
||||
if (!isDeepStrictEqual(newSettings, currentSettings)) {
|
||||
@@ -199,12 +196,6 @@ function startAdminControlsPolling(
|
||||
onSettingsChanged(newSettings);
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-enterprise users don't have access to fetch settings.
|
||||
if (isGaxiosError(e) && e.status === 403) {
|
||||
stopAdminControlsPolling();
|
||||
currentSettings = undefined;
|
||||
return;
|
||||
}
|
||||
debugLogger.error('Failed to poll admin controls: ', e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ export const ExperimentFlags = {
|
||||
MASKING_PROTECTION_THRESHOLD: 45758817,
|
||||
MASKING_PRUNABLE_THRESHOLD: 45758818,
|
||||
MASKING_PROTECT_LATEST_TURN: 45758819,
|
||||
GEMINI_3_1_PRO_LAUNCHED: 45760185,
|
||||
} as const;
|
||||
|
||||
export type ExperimentFlagName =
|
||||
|
||||
@@ -355,4 +355,5 @@ export const FetchAdminControlsResponseSchema = z.object({
|
||||
strictModeDisabled: z.boolean().optional(),
|
||||
mcpSetting: McpSettingSchema.optional(),
|
||||
cliFeatureSetting: CliFeatureSettingSchema.optional(),
|
||||
adminControlsApplicable: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -41,7 +41,6 @@ import type { SkillDefinition } from '../skills/skillLoader.js';
|
||||
import type { McpClientManager } from '../tools/mcp-client-manager.js';
|
||||
import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js';
|
||||
import { DEFAULT_GEMINI_MODEL } from './models.js';
|
||||
import { Storage } from './storage.js';
|
||||
|
||||
vi.mock('fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('fs')>();
|
||||
@@ -280,21 +279,16 @@ describe('Server Config (config.ts)', () => {
|
||||
await expect(config.initialize()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should deduplicate multiple calls to initialize', async () => {
|
||||
it('should throw an error if initialized more than once', async () => {
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
checkpointing: false,
|
||||
});
|
||||
|
||||
const storageSpy = vi.spyOn(Storage.prototype, 'initialize');
|
||||
|
||||
await Promise.all([
|
||||
config.initialize(),
|
||||
config.initialize(),
|
||||
config.initialize(),
|
||||
]);
|
||||
|
||||
expect(storageSpy).toHaveBeenCalledTimes(1);
|
||||
await expect(config.initialize()).resolves.toBeUndefined();
|
||||
await expect(config.initialize()).rejects.toThrow(
|
||||
'Config was already initialized',
|
||||
);
|
||||
});
|
||||
|
||||
it('should await MCP initialization in non-interactive mode', async () => {
|
||||
@@ -2589,34 +2583,4 @@ describe('syncPlanModeTools', () => {
|
||||
|
||||
expect(setToolsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('user hints', () => {
|
||||
it('stores trimmed hints and exposes them via indexing', () => {
|
||||
const config = new Config(baseParams);
|
||||
|
||||
config.addUserHint(' first hint ');
|
||||
config.addUserHint('second hint');
|
||||
config.addUserHint(' ');
|
||||
|
||||
expect(config.getUserHints()).toEqual(['first hint', 'second hint']);
|
||||
expect(config.getLatestHintIndex()).toBe(1);
|
||||
expect(config.getUserHintsAfter(-1)).toEqual([
|
||||
'first hint',
|
||||
'second hint',
|
||||
]);
|
||||
expect(config.getUserHintsAfter(0)).toEqual(['second hint']);
|
||||
expect(config.getUserHintsAfter(1)).toEqual([]);
|
||||
});
|
||||
|
||||
it('tracks the last hint timestamp', () => {
|
||||
const config = new Config(baseParams);
|
||||
|
||||
expect(config.getLastUserHintAt()).toBeNull();
|
||||
config.addUserHint('hint');
|
||||
|
||||
const timestamp = config.getLastUserHintAt();
|
||||
expect(timestamp).not.toBeNull();
|
||||
expect(typeof timestamp).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+127
-115
@@ -615,7 +615,7 @@ export class Config {
|
||||
private readonly enablePromptCompletion: boolean = false;
|
||||
private readonly truncateToolOutputThreshold: number;
|
||||
private compressionTruncationCounter = 0;
|
||||
private initPromise: Promise<void> | undefined;
|
||||
private initialized: boolean = false;
|
||||
readonly storage: Storage;
|
||||
private readonly fileExclusions: FileExclusions;
|
||||
private readonly eventEmitter?: EventEmitter;
|
||||
@@ -668,7 +668,7 @@ export class Config {
|
||||
private remoteAdminSettings: AdminControlsSettings | undefined;
|
||||
private latestApiRequest: GenerateContentParameters | undefined;
|
||||
private lastModeSwitchTime: number = Date.now();
|
||||
private userHints: Array<{ text: string; timestamp: number }> = [];
|
||||
|
||||
private approvedPlanPath: string | undefined;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
@@ -909,100 +909,97 @@ export class Config {
|
||||
* Must only be called once, throws if called again.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initPromise) {
|
||||
return this.initPromise;
|
||||
if (this.initialized) {
|
||||
throw Error('Config was already initialized');
|
||||
}
|
||||
this.initialized = true;
|
||||
|
||||
await this.storage.initialize();
|
||||
|
||||
// Add pending directories to workspace context
|
||||
for (const dir of this.pendingIncludeDirectories) {
|
||||
this.workspaceContext.addDirectory(dir);
|
||||
}
|
||||
|
||||
this.initPromise = (async () => {
|
||||
await this.storage.initialize();
|
||||
// Add plans directory to workspace context for plan file storage
|
||||
if (this.planEnabled) {
|
||||
const plansDir = this.storage.getProjectTempPlansDir();
|
||||
await fs.promises.mkdir(plansDir, { recursive: true });
|
||||
this.workspaceContext.addDirectory(plansDir);
|
||||
}
|
||||
|
||||
// Add pending directories to workspace context
|
||||
for (const dir of this.pendingIncludeDirectories) {
|
||||
this.workspaceContext.addDirectory(dir);
|
||||
}
|
||||
// Initialize centralized FileDiscoveryService
|
||||
const discoverToolsHandle = startupProfiler.start('discover_tools');
|
||||
this.getFileService();
|
||||
if (this.getCheckpointingEnabled()) {
|
||||
await this.getGitService();
|
||||
}
|
||||
this.promptRegistry = new PromptRegistry();
|
||||
this.resourceRegistry = new ResourceRegistry();
|
||||
|
||||
// Add plans directory to workspace context for plan file storage
|
||||
if (this.planEnabled) {
|
||||
const plansDir = this.storage.getProjectTempPlansDir();
|
||||
await fs.promises.mkdir(plansDir, { recursive: true });
|
||||
this.workspaceContext.addDirectory(plansDir);
|
||||
}
|
||||
this.agentRegistry = new AgentRegistry(this);
|
||||
await this.agentRegistry.initialize();
|
||||
|
||||
// Initialize centralized FileDiscoveryService
|
||||
const discoverToolsHandle = startupProfiler.start('discover_tools');
|
||||
this.getFileService();
|
||||
if (this.getCheckpointingEnabled()) {
|
||||
await this.getGitService();
|
||||
}
|
||||
this.promptRegistry = new PromptRegistry();
|
||||
this.resourceRegistry = new ResourceRegistry();
|
||||
coreEvents.on(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed);
|
||||
|
||||
this.agentRegistry = new AgentRegistry(this);
|
||||
await this.agentRegistry.initialize();
|
||||
|
||||
coreEvents.on(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed);
|
||||
|
||||
this.toolRegistry = await this.createToolRegistry();
|
||||
discoverToolsHandle?.end();
|
||||
this.mcpClientManager = new McpClientManager(
|
||||
this.clientVersion,
|
||||
this.toolRegistry,
|
||||
this,
|
||||
this.eventEmitter,
|
||||
);
|
||||
// We do not await this promise so that the CLI can start up even if
|
||||
// MCP servers are slow to connect.
|
||||
const mcpInitialization = Promise.allSettled([
|
||||
this.mcpClientManager.startConfiguredMcpServers(),
|
||||
this.getExtensionLoader().start(this),
|
||||
]).then((results) => {
|
||||
for (const result of results) {
|
||||
if (result.status === 'rejected') {
|
||||
debugLogger.error('Error initializing MCP clients:', result.reason);
|
||||
}
|
||||
this.toolRegistry = await this.createToolRegistry();
|
||||
discoverToolsHandle?.end();
|
||||
this.mcpClientManager = new McpClientManager(
|
||||
this.clientVersion,
|
||||
this.toolRegistry,
|
||||
this,
|
||||
this.eventEmitter,
|
||||
);
|
||||
// We do not await this promise so that the CLI can start up even if
|
||||
// MCP servers are slow to connect.
|
||||
const mcpInitialization = Promise.allSettled([
|
||||
this.mcpClientManager.startConfiguredMcpServers(),
|
||||
this.getExtensionLoader().start(this),
|
||||
]).then((results) => {
|
||||
for (const result of results) {
|
||||
if (result.status === 'rejected') {
|
||||
debugLogger.error('Error initializing MCP clients:', result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.interactive) {
|
||||
await mcpInitialization;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.skillsSupport) {
|
||||
this.getSkillManager().setAdminSettings(this.adminSkillsEnabled);
|
||||
if (this.adminSkillsEnabled) {
|
||||
await this.getSkillManager().discoverSkills(
|
||||
this.storage,
|
||||
this.getExtensions(),
|
||||
this.isTrustedFolder(),
|
||||
if (!this.interactive) {
|
||||
await mcpInitialization;
|
||||
}
|
||||
|
||||
if (this.skillsSupport) {
|
||||
this.getSkillManager().setAdminSettings(this.adminSkillsEnabled);
|
||||
if (this.adminSkillsEnabled) {
|
||||
await this.getSkillManager().discoverSkills(
|
||||
this.storage,
|
||||
this.getExtensions(),
|
||||
this.isTrustedFolder(),
|
||||
);
|
||||
this.getSkillManager().setDisabledSkills(this.disabledSkills);
|
||||
|
||||
// Re-register ActivateSkillTool to update its schema with the discovered enabled skill enums
|
||||
if (this.getSkillManager().getSkills().length > 0) {
|
||||
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
|
||||
this.getToolRegistry().registerTool(
|
||||
new ActivateSkillTool(this, this.messageBus),
|
||||
);
|
||||
this.getSkillManager().setDisabledSkills(this.disabledSkills);
|
||||
|
||||
// Re-register ActivateSkillTool to update its schema with the discovered enabled skill enums
|
||||
if (this.getSkillManager().getSkills().length > 0) {
|
||||
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
|
||||
this.getToolRegistry().registerTool(
|
||||
new ActivateSkillTool(this, this.messageBus),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize hook system if enabled
|
||||
if (this.getEnableHooks()) {
|
||||
this.hookSystem = new HookSystem(this);
|
||||
await this.hookSystem.initialize();
|
||||
}
|
||||
// Initialize hook system if enabled
|
||||
if (this.getEnableHooks()) {
|
||||
this.hookSystem = new HookSystem(this);
|
||||
await this.hookSystem.initialize();
|
||||
}
|
||||
|
||||
if (this.experimentalJitContext) {
|
||||
this.contextManager = new ContextManager(this);
|
||||
await this.contextManager.refresh();
|
||||
}
|
||||
if (this.experimentalJitContext) {
|
||||
this.contextManager = new ContextManager(this);
|
||||
await this.contextManager.refresh();
|
||||
}
|
||||
|
||||
await this.geminiClient.initialize();
|
||||
this.syncPlanModeTools();
|
||||
})();
|
||||
|
||||
return this.initPromise;
|
||||
await this.geminiClient.initialize();
|
||||
this.syncPlanModeTools();
|
||||
}
|
||||
|
||||
getContentGenerator(): ContentGenerator {
|
||||
@@ -1026,6 +1023,12 @@ export class Config {
|
||||
// Reset availability status when switching auth (e.g. from limited key to OAuth)
|
||||
this.modelAvailabilityService.reset();
|
||||
|
||||
// Clear stale authType to ensure getGemini31LaunchedSync doesn't return stale results
|
||||
// during the transition.
|
||||
if (this.contentGeneratorConfig) {
|
||||
this.contentGeneratorConfig.authType = undefined;
|
||||
}
|
||||
|
||||
const newContentGeneratorConfig = await createContentGeneratorConfig(
|
||||
this,
|
||||
authMethod,
|
||||
@@ -1148,7 +1151,7 @@ export class Config {
|
||||
return this.remoteAdminSettings;
|
||||
}
|
||||
|
||||
setRemoteAdminSettings(settings: AdminControlsSettings): void {
|
||||
setRemoteAdminSettings(settings: AdminControlsSettings | undefined): void {
|
||||
this.remoteAdminSettings = settings;
|
||||
}
|
||||
|
||||
@@ -1301,7 +1304,10 @@ export class Config {
|
||||
if (pooled.remaining !== undefined) {
|
||||
return pooled.remaining;
|
||||
}
|
||||
const primaryModel = resolveModel(this.getModel());
|
||||
const primaryModel = resolveModel(
|
||||
this.getModel(),
|
||||
this.getGemini31LaunchedSync(),
|
||||
);
|
||||
return this.modelQuotas.get(primaryModel)?.remaining;
|
||||
}
|
||||
|
||||
@@ -1310,7 +1316,10 @@ export class Config {
|
||||
if (pooled.limit !== undefined) {
|
||||
return pooled.limit;
|
||||
}
|
||||
const primaryModel = resolveModel(this.getModel());
|
||||
const primaryModel = resolveModel(
|
||||
this.getModel(),
|
||||
this.getGemini31LaunchedSync(),
|
||||
);
|
||||
return this.modelQuotas.get(primaryModel)?.limit;
|
||||
}
|
||||
|
||||
@@ -1319,7 +1328,10 @@ export class Config {
|
||||
if (pooled.resetTime !== undefined) {
|
||||
return pooled.resetTime;
|
||||
}
|
||||
const primaryModel = resolveModel(this.getModel());
|
||||
const primaryModel = resolveModel(
|
||||
this.getModel(),
|
||||
this.getGemini31LaunchedSync(),
|
||||
);
|
||||
return this.modelQuotas.get(primaryModel)?.resetTime;
|
||||
}
|
||||
|
||||
@@ -2148,6 +2160,36 @@ export class Config {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether Gemini 3.1 has been launched.
|
||||
* This method is async and ensures that experiments are loaded before returning the result.
|
||||
*/
|
||||
async getGemini31Launched(): Promise<boolean> {
|
||||
await this.ensureExperimentsLoaded();
|
||||
return this.getGemini31LaunchedSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether Gemini 3.1 has been launched.
|
||||
*
|
||||
* Note: This method should only be called after startup, once experiments have been loaded.
|
||||
* If you need to call this during startup or from an async context, use
|
||||
* getGemini31Launched instead.
|
||||
*/
|
||||
getGemini31LaunchedSync(): boolean {
|
||||
const authType = this.contentGeneratorConfig?.authType;
|
||||
if (
|
||||
authType === AuthType.USE_GEMINI ||
|
||||
authType === AuthType.USE_VERTEX_AI
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
this.experiments?.flags[ExperimentFlags.GEMINI_3_1_PRO_LAUNCHED]
|
||||
?.boolValue ?? false
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureExperimentsLoaded(): Promise<void> {
|
||||
if (!this.experimentsPromise) {
|
||||
return;
|
||||
@@ -2489,36 +2531,6 @@ export class Config {
|
||||
return this.hookSystem;
|
||||
}
|
||||
|
||||
addUserHint(hint: string): void {
|
||||
const trimmed = hint.trim();
|
||||
if (trimmed.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.userHints.push({ text: trimmed, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
getUserHints(): string[] {
|
||||
return this.userHints.map((h) => h.text);
|
||||
}
|
||||
|
||||
getUserHintsAfter(index: number): string[] {
|
||||
if (index < 0) {
|
||||
return this.getUserHints();
|
||||
}
|
||||
return this.userHints.slice(index + 1).map((h) => h.text);
|
||||
}
|
||||
|
||||
getLatestHintIndex(): number {
|
||||
return this.userHints.length - 1;
|
||||
}
|
||||
|
||||
getLastUserHintAt(): number | null {
|
||||
if (this.userHints.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return this.userHints[this.userHints.length - 1].timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hooks configuration
|
||||
*/
|
||||
|
||||
@@ -121,19 +121,6 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = {
|
||||
},
|
||||
},
|
||||
},
|
||||
'flash-lite-helper': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
model: 'gemini-2.5-flash-lite',
|
||||
generateContentConfig: {
|
||||
temperature: 0.2,
|
||||
maxOutputTokens: 120,
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'edit-corrector': {
|
||||
extends: 'base',
|
||||
modelConfig: {
|
||||
|
||||
@@ -23,8 +23,41 @@ import {
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
isActiveModel,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
isPreviewModel,
|
||||
isProModel,
|
||||
} from './models.js';
|
||||
|
||||
describe('isPreviewModel', () => {
|
||||
it('should return true for preview models', () => {
|
||||
expect(isPreviewModel(PREVIEW_GEMINI_MODEL)).toBe(true);
|
||||
expect(isPreviewModel(PREVIEW_GEMINI_3_1_MODEL)).toBe(true);
|
||||
expect(isPreviewModel(PREVIEW_GEMINI_FLASH_MODEL)).toBe(true);
|
||||
expect(isPreviewModel(PREVIEW_GEMINI_MODEL_AUTO)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-preview models', () => {
|
||||
expect(isPreviewModel(DEFAULT_GEMINI_MODEL)).toBe(false);
|
||||
expect(isPreviewModel('gemini-1.5-pro')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isProModel', () => {
|
||||
it('should return true for models containing "pro"', () => {
|
||||
expect(isProModel('gemini-3-pro-preview')).toBe(true);
|
||||
expect(isProModel('gemini-2.5-pro')).toBe(true);
|
||||
expect(isProModel('pro')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for models without "pro"', () => {
|
||||
expect(isProModel('gemini-3-flash-preview')).toBe(false);
|
||||
expect(isProModel('gemini-2.5-flash')).toBe(false);
|
||||
expect(isProModel('auto')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGemini3Model', () => {
|
||||
it('should return true for gemini-3 models', () => {
|
||||
expect(isGemini3Model('gemini-3-pro-preview')).toBe(true);
|
||||
@@ -69,6 +102,12 @@ describe('getDisplayString', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return PREVIEW_GEMINI_3_1_MODEL for PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL', () => {
|
||||
expect(getDisplayString(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL)).toBe(
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the model name as is for other models', () => {
|
||||
expect(getDisplayString('custom-model')).toBe('custom-model');
|
||||
expect(getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL)).toBe(
|
||||
@@ -100,6 +139,16 @@ describe('resolveModel', () => {
|
||||
expect(model).toBe(PREVIEW_GEMINI_MODEL);
|
||||
});
|
||||
|
||||
it('should return Gemini 3.1 Pro when auto-gemini-3 is requested and useGemini3_1 is true', () => {
|
||||
const model = resolveModel(PREVIEW_GEMINI_MODEL_AUTO, true);
|
||||
expect(model).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||
});
|
||||
|
||||
it('should return Gemini 3.1 Pro Custom Tools when auto-gemini-3 is requested, useGemini3_1 is true, and useCustomToolModel is true', () => {
|
||||
const model = resolveModel(PREVIEW_GEMINI_MODEL_AUTO, true, true);
|
||||
expect(model).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
|
||||
});
|
||||
|
||||
it('should return the Default Pro model when auto-gemini-2.5 is requested', () => {
|
||||
const model = resolveModel(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
expect(model).toBe(DEFAULT_GEMINI_MODEL);
|
||||
@@ -193,4 +242,71 @@ describe('resolveClassifierModel', () => {
|
||||
resolveClassifierModel(PREVIEW_GEMINI_MODEL_AUTO, GEMINI_MODEL_ALIAS_PRO),
|
||||
).toBe(PREVIEW_GEMINI_MODEL);
|
||||
});
|
||||
|
||||
it('should return Gemini 3.1 Pro when alias is pro and useGemini3_1 is true', () => {
|
||||
expect(
|
||||
resolveClassifierModel(
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
true,
|
||||
),
|
||||
).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||
});
|
||||
|
||||
it('should return Gemini 3.1 Pro Custom Tools when alias is pro, useGemini3_1 is true, and useCustomToolModel is true', () => {
|
||||
expect(
|
||||
resolveClassifierModel(
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActiveModel', () => {
|
||||
it('should return true for valid models when useGemini3_1 is false', () => {
|
||||
expect(isActiveModel(DEFAULT_GEMINI_MODEL)).toBe(true);
|
||||
expect(isActiveModel(PREVIEW_GEMINI_MODEL)).toBe(true);
|
||||
expect(isActiveModel(DEFAULT_GEMINI_FLASH_MODEL)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for unknown models and aliases', () => {
|
||||
expect(isActiveModel('invalid-model')).toBe(false);
|
||||
expect(isActiveModel(GEMINI_MODEL_ALIAS_AUTO)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for PREVIEW_GEMINI_MODEL when useGemini3_1 is true', () => {
|
||||
expect(isActiveModel(PREVIEW_GEMINI_MODEL, true)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for other valid models when useGemini3_1 is true', () => {
|
||||
expect(isActiveModel(DEFAULT_GEMINI_MODEL, true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly filter Gemini 3.1 models based on useCustomToolModel when useGemini3_1 is true', () => {
|
||||
// When custom tools are preferred, standard 3.1 should be inactive
|
||||
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL, true, true)).toBe(false);
|
||||
expect(
|
||||
isActiveModel(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, true, true),
|
||||
).toBe(true);
|
||||
|
||||
// When custom tools are NOT preferred, custom tools 3.1 should be inactive
|
||||
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL, true, false)).toBe(true);
|
||||
expect(
|
||||
isActiveModel(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, true, false),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for both Gemini 3.1 models when useGemini3_1 is false', () => {
|
||||
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL, false, true)).toBe(false);
|
||||
expect(isActiveModel(PREVIEW_GEMINI_3_1_MODEL, false, false)).toBe(false);
|
||||
expect(
|
||||
isActiveModel(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, false, true),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isActiveModel(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL, false, false),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
*/
|
||||
|
||||
export const PREVIEW_GEMINI_MODEL = 'gemini-3-pro-preview';
|
||||
export const PREVIEW_GEMINI_3_1_MODEL = 'gemini-3.1-pro-preview';
|
||||
export const PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL =
|
||||
'gemini-3.1-pro-preview-customtools';
|
||||
export const PREVIEW_GEMINI_FLASH_MODEL = 'gemini-3-flash-preview';
|
||||
export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
|
||||
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';
|
||||
@@ -12,6 +15,8 @@ export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';
|
||||
|
||||
export const VALID_GEMINI_MODELS = new Set([
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
@@ -37,20 +42,29 @@ export const DEFAULT_THINKING_MODE = 8192;
|
||||
* to a concrete model name.
|
||||
*
|
||||
* @param requestedModel The model alias or concrete model name requested by the user.
|
||||
* @param useGemini3_1 Whether to use Gemini 3.1 Pro Preview for auto/pro aliases.
|
||||
* @returns The resolved concrete model name.
|
||||
*/
|
||||
export function resolveModel(requestedModel: string): string {
|
||||
export function resolveModel(
|
||||
requestedModel: string,
|
||||
useGemini3_1: boolean = false,
|
||||
useCustomToolModel: boolean = false,
|
||||
): string {
|
||||
switch (requestedModel) {
|
||||
case PREVIEW_GEMINI_MODEL_AUTO: {
|
||||
case PREVIEW_GEMINI_MODEL:
|
||||
case PREVIEW_GEMINI_MODEL_AUTO:
|
||||
case GEMINI_MODEL_ALIAS_AUTO:
|
||||
case GEMINI_MODEL_ALIAS_PRO: {
|
||||
if (useGemini3_1) {
|
||||
return useCustomToolModel
|
||||
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
|
||||
: PREVIEW_GEMINI_3_1_MODEL;
|
||||
}
|
||||
return PREVIEW_GEMINI_MODEL;
|
||||
}
|
||||
case DEFAULT_GEMINI_MODEL_AUTO: {
|
||||
return DEFAULT_GEMINI_MODEL;
|
||||
}
|
||||
case GEMINI_MODEL_ALIAS_AUTO:
|
||||
case GEMINI_MODEL_ALIAS_PRO: {
|
||||
return PREVIEW_GEMINI_MODEL;
|
||||
}
|
||||
case GEMINI_MODEL_ALIAS_FLASH: {
|
||||
return PREVIEW_GEMINI_FLASH_MODEL;
|
||||
}
|
||||
@@ -73,6 +87,8 @@ export function resolveModel(requestedModel: string): string {
|
||||
export function resolveClassifierModel(
|
||||
requestedModel: string,
|
||||
modelAlias: string,
|
||||
useGemini3_1: boolean = false,
|
||||
useCustomToolModel: boolean = false,
|
||||
): string {
|
||||
if (modelAlias === GEMINI_MODEL_ALIAS_FLASH) {
|
||||
if (
|
||||
@@ -89,7 +105,7 @@ export function resolveClassifierModel(
|
||||
}
|
||||
return resolveModel(GEMINI_MODEL_ALIAS_FLASH);
|
||||
}
|
||||
return resolveModel(requestedModel);
|
||||
return resolveModel(requestedModel, useGemini3_1, useCustomToolModel);
|
||||
}
|
||||
export function getDisplayString(model: string) {
|
||||
switch (model) {
|
||||
@@ -101,6 +117,8 @@ export function getDisplayString(model: string) {
|
||||
return PREVIEW_GEMINI_MODEL;
|
||||
case GEMINI_MODEL_ALIAS_FLASH:
|
||||
return PREVIEW_GEMINI_FLASH_MODEL;
|
||||
case PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL:
|
||||
return PREVIEW_GEMINI_3_1_MODEL;
|
||||
default:
|
||||
return model;
|
||||
}
|
||||
@@ -115,11 +133,22 @@ export function getDisplayString(model: string) {
|
||||
export function isPreviewModel(model: string): boolean {
|
||||
return (
|
||||
model === PREVIEW_GEMINI_MODEL ||
|
||||
model === PREVIEW_GEMINI_3_1_MODEL ||
|
||||
model === PREVIEW_GEMINI_FLASH_MODEL ||
|
||||
model === PREVIEW_GEMINI_MODEL_AUTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model is a Pro model.
|
||||
*
|
||||
* @param model The model name to check.
|
||||
* @returns True if the model is a Pro model.
|
||||
*/
|
||||
export function isProModel(model: string): boolean {
|
||||
return model.toLowerCase().includes('pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model is a Gemini 3 model.
|
||||
*
|
||||
@@ -165,3 +194,35 @@ export function isAutoModel(model: string): boolean {
|
||||
export function supportsMultimodalFunctionResponse(model: string): boolean {
|
||||
return model.startsWith('gemini-3-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given model is considered active based on the current configuration.
|
||||
*
|
||||
* @param model The model name to check.
|
||||
* @param useGemini3_1 Whether Gemini 3.1 Pro Preview is enabled.
|
||||
* @returns True if the model is active.
|
||||
*/
|
||||
export function isActiveModel(
|
||||
model: string,
|
||||
useGemini3_1: boolean = false,
|
||||
useCustomToolModel: boolean = false,
|
||||
): boolean {
|
||||
if (!VALID_GEMINI_MODELS.has(model)) {
|
||||
return false;
|
||||
}
|
||||
if (useGemini3_1) {
|
||||
if (model === PREVIEW_GEMINI_MODEL) {
|
||||
return false;
|
||||
}
|
||||
if (useCustomToolModel) {
|
||||
return model !== PREVIEW_GEMINI_3_1_MODEL;
|
||||
} else {
|
||||
return model !== PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
model !== PREVIEW_GEMINI_3_1_MODEL &&
|
||||
model !== PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > Appro
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -143,7 +142,6 @@ exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > Appro
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -280,7 +278,6 @@ exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > shoul
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -393,7 +390,6 @@ exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > shoul
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -530,7 +526,7 @@ exports[`Core System Prompt (prompts.ts) > should append userMemory with separat
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -657,7 +653,7 @@ exports[`Core System Prompt (prompts.ts) > should handle CodebaseInvestigator wi
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, you must work autonomously as no further user input is available. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Handle Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, do not perform it automatically.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -749,7 +745,7 @@ exports[`Core System Prompt (prompts.ts) > should handle CodebaseInvestigator wi
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, you must work autonomously as no further user input is available. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Handle Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, do not perform it automatically.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -836,7 +832,6 @@ exports[`Core System Prompt (prompts.ts) > should handle git instructions when i
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -949,7 +944,6 @@ exports[`Core System Prompt (prompts.ts) > should handle git instructions when i
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -1080,7 +1074,6 @@ exports[`Core System Prompt (prompts.ts) > should include approved plan instruct
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -1183,7 +1176,6 @@ exports[`Core System Prompt (prompts.ts) > should include available_skills when
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -1314,7 +1306,7 @@ exports[`Core System Prompt (prompts.ts) > should include correct sandbox instru
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -1428,7 +1420,7 @@ exports[`Core System Prompt (prompts.ts) > should include correct sandbox instru
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -1542,7 +1534,7 @@ exports[`Core System Prompt (prompts.ts) > should include correct sandbox instru
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -1652,7 +1644,7 @@ exports[`Core System Prompt (prompts.ts) > should include planning phase suggest
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -1762,7 +1754,7 @@ exports[`Core System Prompt (prompts.ts) > should include sub-agents in XML for
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -1867,7 +1859,6 @@ exports[`Core System Prompt (prompts.ts) > should match snapshot on Windows 1`]
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -1981,7 +1972,6 @@ exports[`Core System Prompt (prompts.ts) > should render hierarchical memory wit
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **Conflict Resolution:** Instructions are provided in hierarchical context tags: \`<global_context>\`, \`<extension_context>\`, and \`<project_context>\`. In case of contradictory instructions, follow this priority: \`<project_context>\` (highest) > \`<extension_context>\` > \`<global_context>\` (lowest).
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -2113,7 +2103,7 @@ exports[`Core System Prompt (prompts.ts) > should return the base prompt when us
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -2223,7 +2213,7 @@ exports[`Core System Prompt (prompts.ts) > should return the base prompt when us
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -2328,7 +2318,6 @@ exports[`Core System Prompt (prompts.ts) > should return the interactive avoidan
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Handle Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, do not perform it automatically.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -2445,7 +2434,7 @@ exports[`Core System Prompt (prompts.ts) > should use chatty system prompt for p
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -2555,7 +2544,7 @@ exports[`Core System Prompt (prompts.ts) > should use chatty system prompt for p
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
@@ -2660,7 +2649,6 @@ exports[`Core System Prompt (prompts.ts) > should use legacy system prompt for n
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If the user implies a change (e.g., reports a bug) without explicitly asking for a fix, **ask for confirmation first**. If asked *how* to do something, explain first, don't just do it.
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
|
||||
|
||||
@@ -541,7 +541,10 @@ export class GeminiClient {
|
||||
|
||||
// Availability logic: The configured model is the source of truth,
|
||||
// including any permanent fallbacks (config.setModel) or manual overrides.
|
||||
return resolveModel(this.config.getActiveModel());
|
||||
return resolveModel(
|
||||
this.config.getActiveModel(),
|
||||
this.config.getGemini31LaunchedSync?.() ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
private async *processTurn(
|
||||
|
||||
@@ -122,7 +122,12 @@ export async function createContentGenerator(
|
||||
return new LoggingContentGenerator(fakeGenerator, gcConfig);
|
||||
}
|
||||
const version = await getVersion();
|
||||
const model = resolveModel(gcConfig.getModel());
|
||||
const model = resolveModel(
|
||||
gcConfig.getModel(),
|
||||
config.authType === AuthType.USE_GEMINI ||
|
||||
config.authType === AuthType.USE_VERTEX_AI ||
|
||||
((await gcConfig.getGemini31Launched?.()) ?? false),
|
||||
);
|
||||
const customHeadersEnv =
|
||||
process.env['GEMINI_CLI_CUSTOM_HEADERS'] || undefined;
|
||||
const userAgent = `GeminiCLI/${version}/${model} (${process.platform}; ${process.arch})`;
|
||||
|
||||
@@ -492,13 +492,14 @@ export class GeminiChat {
|
||||
const initialActiveModel = this.config.getActiveModel();
|
||||
|
||||
const apiCall = async () => {
|
||||
const useGemini3_1 = (await this.config.getGemini31Launched?.()) ?? false;
|
||||
// Default to the last used model (which respects arguments/availability selection)
|
||||
let modelToUse = resolveModel(lastModelToUse);
|
||||
let modelToUse = resolveModel(lastModelToUse, useGemini3_1);
|
||||
|
||||
// If the active model has changed (e.g. due to a fallback updating the config),
|
||||
// we switch to the new active model.
|
||||
if (this.config.getActiveModel() !== initialActiveModel) {
|
||||
modelToUse = resolveModel(this.config.getActiveModel());
|
||||
modelToUse = resolveModel(this.config.getActiveModel(), useGemini3_1);
|
||||
}
|
||||
|
||||
if (modelToUse !== lastModelToUse) {
|
||||
|
||||
@@ -198,7 +198,6 @@ describe('Core System Prompt (prompts.ts)', () => {
|
||||
expect(prompt).not.toContain('No sub-agents are currently available.');
|
||||
expect(prompt).toContain('# Core Mandates');
|
||||
expect(prompt).toContain('- **Conventions:**');
|
||||
expect(prompt).toContain('- **User Hints:**');
|
||||
expect(prompt).toContain('# Outside of Sandbox');
|
||||
expect(prompt).toContain('# Final Reminder');
|
||||
expect(prompt).toMatchSnapshot();
|
||||
@@ -208,7 +207,6 @@ describe('Core System Prompt (prompts.ts)', () => {
|
||||
vi.mocked(mockConfig.getActiveModel).mockReturnValue(PREVIEW_GEMINI_MODEL);
|
||||
const prompt = getCoreSystemPrompt(mockConfig);
|
||||
expect(prompt).toContain('You are Gemini CLI, an interactive CLI agent'); // Check for core content
|
||||
expect(prompt).toContain('- **User Hints:**');
|
||||
expect(prompt).toContain('No Chitchat:');
|
||||
expect(prompt).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ export * from './commands/memory.js';
|
||||
export * from './commands/types.js';
|
||||
|
||||
// Export Core Logic
|
||||
export * from './core/baseLlmClient.js';
|
||||
export * from './core/client.js';
|
||||
export * from './core/contentGenerator.js';
|
||||
export * from './core/loggingContentGenerator.js';
|
||||
@@ -89,7 +88,6 @@ export * from './utils/formatters.js';
|
||||
export * from './utils/generateContentResponseUtilities.js';
|
||||
export * from './utils/filesearch/fileSearch.js';
|
||||
export * from './utils/errorParsing.js';
|
||||
export * from './utils/flashLiteHelper.js';
|
||||
export * from './utils/workspaceContext.js';
|
||||
export * from './utils/environmentContext.js';
|
||||
export * from './utils/ignorePatterns.js';
|
||||
|
||||
@@ -58,7 +58,10 @@ export class PromptProvider {
|
||||
const enabledToolNames = new Set(toolNames);
|
||||
const approvedPlanPath = config.getApprovedPlanPath();
|
||||
|
||||
const desiredModel = resolveModel(config.getActiveModel());
|
||||
const desiredModel = resolveModel(
|
||||
config.getActiveModel(),
|
||||
config.getGemini31LaunchedSync?.() ?? false,
|
||||
);
|
||||
const isGemini3 = isPreviewModel(desiredModel);
|
||||
const activeSnippets = isGemini3 ? snippets : legacySnippets;
|
||||
const contextFilenames = getAllGeminiMdFilenames();
|
||||
@@ -233,7 +236,10 @@ export class PromptProvider {
|
||||
}
|
||||
|
||||
getCompressionPrompt(config: Config): string {
|
||||
const desiredModel = resolveModel(config.getActiveModel());
|
||||
const desiredModel = resolveModel(
|
||||
config.getActiveModel(),
|
||||
config.getGemini31LaunchedSync?.() ?? false,
|
||||
);
|
||||
const isGemini3 = isPreviewModel(desiredModel);
|
||||
const activeSnippets = isGemini3 ? snippets : legacySnippets;
|
||||
return activeSnippets.getCompressionPrompt();
|
||||
|
||||
@@ -159,7 +159,6 @@ export function renderCoreMandates(options?: CoreMandatesOptions): string {
|
||||
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
|
||||
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
|
||||
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.${mandateConflictResolution(options.hasHierarchicalMemory)}
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- ${mandateConfirm(options.interactive)}
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.${mandateSkillGuidance(options.hasSkills)}${mandateExplainBeforeActing(options.isGemini3)}${mandateContinueWork(options.interactive)}
|
||||
|
||||
@@ -170,8 +170,8 @@ export function renderCoreMandates(options?: CoreMandatesOptions): string {
|
||||
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
|
||||
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
|
||||
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. ${options.interactive ? 'For Directives, only clarify if critically underspecified; otherwise, work autonomously.' : 'For Directives, you must work autonomously as no further user input is available.'} You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.${mandateConflictResolution(options.hasHierarchicalMemory)}
|
||||
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
|
||||
- **Proactiveness:** When executing a Directive, persist through errors and obstacles by diagnosing failures in the execution phase and, if necessary, backtracking to the research or strategy phases to adjust your approach until a successful, verified outcome is achieved. Fulfill the user's request thoroughly, including adding tests when adding features or fixing bugs. Take reasonable liberties to fulfill broad goals while staying within the requested scope; however, prioritize simplicity and the removal of redundant logic over providing "just-in-case" alternatives that diverge from the established path.
|
||||
- **Testing:** ALWAYS search for and update related tests after making a code change. You must add a new test case to the existing test file (if one exists) or create a new test file to verify your changes.${mandateConflictResolution(options.hasHierarchicalMemory)}
|
||||
- ${mandateConfirm(options.interactive)}
|
||||
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
|
||||
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.${mandateSkillGuidance(options.hasSkills)}
|
||||
|
||||
@@ -18,11 +18,14 @@ import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
} from '../../config/models.js';
|
||||
import { promptIdContext } from '../../utils/promptIdContext.js';
|
||||
import type { Content } from '@google/genai';
|
||||
import type { ResolvedModelConfig } from '../../services/modelConfigService.js';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
import { AuthType } from '../../core/contentGenerator.js';
|
||||
|
||||
vi.mock('../../core/baseLlmClient.js');
|
||||
|
||||
@@ -53,6 +56,10 @@ describe('ClassifierStrategy', () => {
|
||||
},
|
||||
getModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO),
|
||||
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(false),
|
||||
getGemini31Launched: vi.fn().mockResolvedValue(false),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
}),
|
||||
} as unknown as Config;
|
||||
mockBaseLlmClient = {
|
||||
generateJson: vi.fn(),
|
||||
@@ -339,4 +346,49 @@ describe('ClassifierStrategy', () => {
|
||||
// Since requestedModel is Pro, and choice is flash, it should resolve to Flash
|
||||
expect(decision?.model).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
});
|
||||
|
||||
describe('Gemini 3.1 and Custom Tools Routing', () => {
|
||||
it('should route to PREVIEW_GEMINI_3_1_MODEL when Gemini 3.1 is launched', async () => {
|
||||
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue(PREVIEW_GEMINI_MODEL_AUTO);
|
||||
const mockApiResponse = {
|
||||
reasoning: 'Complex task',
|
||||
model_choice: 'pro',
|
||||
};
|
||||
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
);
|
||||
|
||||
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||
});
|
||||
|
||||
it('should route to PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL when Gemini 3.1 is launched and auth is USE_GEMINI', async () => {
|
||||
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue(PREVIEW_GEMINI_MODEL_AUTO);
|
||||
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
});
|
||||
const mockApiResponse = {
|
||||
reasoning: 'Complex task',
|
||||
model_choice: 'pro',
|
||||
};
|
||||
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
);
|
||||
|
||||
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
isFunctionResponse,
|
||||
} from '../../utils/messageInspectors.js';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
import { AuthType } from '../../core/contentGenerator.js';
|
||||
|
||||
// The number of recent history turns to provide to the router for context.
|
||||
const HISTORY_TURNS_FOR_CONTEXT = 4;
|
||||
@@ -167,9 +168,15 @@ export class ClassifierStrategy implements RoutingStrategy {
|
||||
|
||||
const reasoning = routerResponse.reasoning;
|
||||
const latencyMs = Date.now() - startTime;
|
||||
const useGemini3_1 = (await config.getGemini31Launched?.()) ?? false;
|
||||
const useCustomToolModel =
|
||||
useGemini3_1 &&
|
||||
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI;
|
||||
const selectedModel = resolveClassifierModel(
|
||||
model,
|
||||
routerResponse.model_choice,
|
||||
useGemini3_1,
|
||||
useCustomToolModel,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,10 @@ export class DefaultStrategy implements TerminalStrategy {
|
||||
config: Config,
|
||||
_baseLlmClient: BaseLlmClient,
|
||||
): Promise<RoutingDecision> {
|
||||
const defaultModel = resolveModel(config.getModel());
|
||||
const defaultModel = resolveModel(
|
||||
config.getModel(),
|
||||
config.getGemini31LaunchedSync?.() ?? false,
|
||||
);
|
||||
return {
|
||||
model: defaultModel,
|
||||
metadata: {
|
||||
|
||||
@@ -23,7 +23,10 @@ export class FallbackStrategy implements RoutingStrategy {
|
||||
_baseLlmClient: BaseLlmClient,
|
||||
): Promise<RoutingDecision | null> {
|
||||
const requestedModel = context.requestedModel ?? config.getModel();
|
||||
const resolvedModel = resolveModel(requestedModel);
|
||||
const resolvedModel = resolveModel(
|
||||
requestedModel,
|
||||
config.getGemini31LaunchedSync?.() ?? false,
|
||||
);
|
||||
const service = config.getModelAvailabilityService();
|
||||
const snapshot = service.snapshot(resolvedModel);
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { BaseLlmClient } from '../../core/baseLlmClient.js';
|
||||
import {
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
@@ -20,6 +22,7 @@ import { promptIdContext } from '../../utils/promptIdContext.js';
|
||||
import type { Content } from '@google/genai';
|
||||
import type { ResolvedModelConfig } from '../../services/modelConfigService.js';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
import { AuthType } from '../../core/contentGenerator.js';
|
||||
|
||||
vi.mock('../../core/baseLlmClient.js');
|
||||
|
||||
@@ -52,6 +55,10 @@ describe('NumericalClassifierStrategy', () => {
|
||||
getSessionId: vi.fn().mockReturnValue('control-group-id'), // Default to Control Group (Hash 71 >= 50)
|
||||
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(true),
|
||||
getClassifierThreshold: vi.fn().mockResolvedValue(undefined),
|
||||
getGemini31Launched: vi.fn().mockResolvedValue(false),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
}),
|
||||
} as unknown as Config;
|
||||
mockBaseLlmClient = {
|
||||
generateJson: vi.fn(),
|
||||
@@ -535,4 +542,68 @@ describe('NumericalClassifierStrategy', () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe('Gemini 3.1 and Custom Tools Routing', () => {
|
||||
it('should route to PREVIEW_GEMINI_3_1_MODEL when Gemini 3.1 is launched', async () => {
|
||||
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||
const mockApiResponse = {
|
||||
complexity_reasoning: 'Complex task',
|
||||
complexity_score: 80,
|
||||
};
|
||||
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
);
|
||||
|
||||
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||
});
|
||||
it('should route to PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL when Gemini 3.1 is launched and auth is USE_GEMINI', async () => {
|
||||
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
});
|
||||
const mockApiResponse = {
|
||||
complexity_reasoning: 'Complex task',
|
||||
complexity_score: 80,
|
||||
};
|
||||
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
);
|
||||
|
||||
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL);
|
||||
});
|
||||
|
||||
it('should NOT route to custom tools model when auth is USE_VERTEX_AI', async () => {
|
||||
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
|
||||
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
|
||||
authType: AuthType.USE_VERTEX_AI,
|
||||
});
|
||||
const mockApiResponse = {
|
||||
complexity_reasoning: 'Complex task',
|
||||
complexity_score: 80,
|
||||
};
|
||||
vi.mocked(mockBaseLlmClient.generateJson).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const decision = await strategy.route(
|
||||
mockContext,
|
||||
mockConfig,
|
||||
mockBaseLlmClient,
|
||||
);
|
||||
|
||||
expect(decision?.model).toBe(PREVIEW_GEMINI_3_1_MODEL);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import { resolveClassifierModel, isGemini3Model } from '../../config/models.js';
|
||||
import { createUserContent, Type } from '@google/genai';
|
||||
import type { Config } from '../../config/config.js';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
import { AuthType } from '../../core/contentGenerator.js';
|
||||
|
||||
// The number of recent history turns to provide to the router for context.
|
||||
const HISTORY_TURNS_FOR_CONTEXT = 8;
|
||||
@@ -180,8 +181,16 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
|
||||
config,
|
||||
config.getSessionId() || 'unknown-session',
|
||||
);
|
||||
|
||||
const selectedModel = resolveClassifierModel(model, modelAlias);
|
||||
const useGemini3_1 = (await config.getGemini31Launched?.()) ?? false;
|
||||
const useCustomToolModel =
|
||||
useGemini3_1 &&
|
||||
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI;
|
||||
const selectedModel = resolveClassifierModel(
|
||||
model,
|
||||
modelAlias,
|
||||
useGemini3_1,
|
||||
useCustomToolModel,
|
||||
);
|
||||
|
||||
const latencyMs = Date.now() - startTime;
|
||||
|
||||
|
||||
@@ -33,7 +33,10 @@ export class OverrideStrategy implements RoutingStrategy {
|
||||
|
||||
// Return the overridden model name.
|
||||
return {
|
||||
model: resolveModel(overrideModel),
|
||||
model: resolveModel(
|
||||
overrideModel,
|
||||
config.getGemini31LaunchedSync?.() ?? false,
|
||||
),
|
||||
metadata: {
|
||||
source: this.name,
|
||||
latencyMs: 0,
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
} from '../config/models.js';
|
||||
import { PreCompressTrigger } from '../hooks/types.js';
|
||||
|
||||
@@ -100,6 +101,7 @@ export function findCompressSplitPoint(
|
||||
export function modelStringToModelConfigAlias(model: string): string {
|
||||
switch (model) {
|
||||
case PREVIEW_GEMINI_MODEL:
|
||||
case PREVIEW_GEMINI_3_1_MODEL:
|
||||
return 'chat-compression-3-pro';
|
||||
case PREVIEW_GEMINI_FLASH_MODEL:
|
||||
return 'chat-compression-3-flash';
|
||||
|
||||
@@ -46,9 +46,6 @@ describe('sanitizeEnvironment', () => {
|
||||
CLIENT_ID: 'sensitive-id',
|
||||
DB_URI: 'sensitive-uri',
|
||||
DATABASE_URL: 'sensitive-url',
|
||||
GEMINI_API_KEY: 'sensitive-gemini-key',
|
||||
GOOGLE_API_KEY: 'sensitive-google-key',
|
||||
GOOGLE_APPLICATION_CREDENTIALS: '/path/to/creds.json',
|
||||
SAFE_VAR: 'is-safe',
|
||||
};
|
||||
const sanitized = sanitizeEnvironment(env, EMPTY_OPTIONS);
|
||||
|
||||
@@ -103,9 +103,6 @@ export const NEVER_ALLOWED_ENVIRONMENT_VARIABLES: ReadonlySet<string> = new Set(
|
||||
'GOOGLE_CLOUD_PROJECT',
|
||||
'GOOGLE_CLOUD_ACCOUNT',
|
||||
'FIREBASE_PROJECT_ID',
|
||||
'GEMINI_API_KEY',
|
||||
'GOOGLE_API_KEY',
|
||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -126,17 +126,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"flash-lite-helper": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0.2,
|
||||
"topP": 1,
|
||||
"maxOutputTokens": 120,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit-corrector": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
|
||||
@@ -126,17 +126,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"flash-lite-helper": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
"temperature": 0.2,
|
||||
"topP": 1,
|
||||
"maxOutputTokens": 120,
|
||||
"thinkingConfig": {
|
||||
"thinkingBudget": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit-corrector": {
|
||||
"model": "gemini-2.5-flash-lite",
|
||||
"generateContentConfig": {
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('loggers', () => {
|
||||
});
|
||||
|
||||
describe('logCliConfiguration', () => {
|
||||
it('should log the cli configuration', () => {
|
||||
it('should log the cli configuration', async () => {
|
||||
const mockConfig = {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getModel: () => 'test-model',
|
||||
@@ -226,11 +226,14 @@ describe('loggers', () => {
|
||||
}),
|
||||
}),
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
const startSessionEvent = new StartSessionEvent(mockConfig);
|
||||
logCliConfiguration(mockConfig, startSessionEvent);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
expect(mockLogger.emit).toHaveBeenCalledWith({
|
||||
body: 'CLI configuration loaded.',
|
||||
attributes: {
|
||||
@@ -271,6 +274,8 @@ describe('loggers', () => {
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should log a user prompt', () => {
|
||||
@@ -308,6 +313,8 @@ describe('loggers', () => {
|
||||
getTargetDir: () => 'target-dir',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
const event = new UserPromptEvent(
|
||||
11,
|
||||
@@ -343,6 +350,8 @@ describe('loggers', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
@@ -519,6 +528,8 @@ describe('loggers', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
@@ -651,6 +662,8 @@ describe('loggers', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => ({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
}),
|
||||
@@ -727,6 +740,8 @@ describe('loggers', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true, // Enabled
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => ({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
}),
|
||||
@@ -814,6 +829,8 @@ describe('loggers', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => false, // Disabled
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getContentGeneratorConfig: () => ({
|
||||
authType: AuthType.USE_VERTEX_AI,
|
||||
}),
|
||||
@@ -867,6 +884,8 @@ describe('loggers', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getContentGeneratorConfig: () => ({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
@@ -903,6 +922,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should log flash fallback event', () => {
|
||||
@@ -930,6 +951,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1024,6 +1047,8 @@ describe('loggers', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
@@ -1595,6 +1620,8 @@ describe('loggers', () => {
|
||||
getTelemetryEnabled: () => true,
|
||||
getTelemetryLogPromptsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as Config;
|
||||
|
||||
const mockMetrics = {
|
||||
@@ -1655,6 +1682,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
it('should log a tool output truncated event', () => {
|
||||
@@ -1692,6 +1721,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1792,6 +1823,8 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getContentGeneratorConfig: () => null,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1842,6 +1875,8 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getContentGeneratorConfig: () => null,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1894,6 +1929,8 @@ describe('loggers', () => {
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
getContentGeneratorConfig: () => null,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1938,6 +1975,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1983,6 +2022,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2028,6 +2069,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2064,6 +2107,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2115,6 +2160,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2150,6 +2197,8 @@ describe('loggers', () => {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getUsageStatisticsEnabled: () => true,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getTelemetryLogPromptsEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
@@ -2205,7 +2254,7 @@ describe('loggers', () => {
|
||||
});
|
||||
|
||||
describe('Telemetry Buffering', () => {
|
||||
it('should buffer events when SDK is not initialized', () => {
|
||||
it('should buffer events when SDK is not initialized', async () => {
|
||||
vi.spyOn(sdk, 'isTelemetrySdkInitialized').mockReturnValue(false);
|
||||
const bufferSpy = vi
|
||||
.spyOn(sdk, 'bufferTelemetryEvent')
|
||||
|
||||
@@ -81,6 +81,7 @@ import { bufferTelemetryEvent } from './sdk.js';
|
||||
import type { UiEvent } from './uiTelemetry.js';
|
||||
import { uiTelemetryService } from './uiTelemetry.js';
|
||||
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
export function logCliConfiguration(
|
||||
config: Config,
|
||||
@@ -88,12 +89,20 @@ export function logCliConfiguration(
|
||||
): void {
|
||||
void ClearcutLogger.getInstance(config)?.logStartSessionEvent(event);
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
// Wait for experiments to load before emitting so we capture experimentIds
|
||||
void config
|
||||
.getExperimentsAsync()
|
||||
.then(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
debugLogger.error('Failed to log telemetry event', e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -780,11 +789,19 @@ export function logStartupStats(
|
||||
event: StartupStatsEvent,
|
||||
): void {
|
||||
bufferTelemetryEvent(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
// Wait for experiments to load before emitting so we capture experimentIds
|
||||
void config
|
||||
.getExperimentsAsync()
|
||||
.then(() => {
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: event.toLogBody(),
|
||||
attributes: event.toOpenTelemetryAttributes(config),
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
debugLogger.error('Failed to log telemetry event', e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ function createMockConfig(logPromptsEnabled: boolean): Config {
|
||||
return {
|
||||
getTelemetryLogPromptsEnabled: () => logPromptsEnabled,
|
||||
getSessionId: () => 'test-session-id',
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
getModel: () => 'gemini-1.5-flash',
|
||||
isInteractive: () => true,
|
||||
getUserEmail: () => undefined,
|
||||
|
||||
@@ -75,6 +75,8 @@ describe('Telemetry SDK', () => {
|
||||
getSessionId: () => 'test-session',
|
||||
getTelemetryUseCliAuth: () => false,
|
||||
isInteractive: () => false,
|
||||
getExperiments: () => undefined,
|
||||
getExperimentsAsync: async () => undefined,
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
@@ -353,6 +355,7 @@ describe('Telemetry SDK', () => {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await initializeTelemetry(mockConfig);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -243,15 +243,6 @@ export class StartupProfiler {
|
||||
// Clear all phases.
|
||||
this.phases.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the profiler state for tests.
|
||||
*/
|
||||
clear(): void {
|
||||
this.phases.clear();
|
||||
performance.clearMarks();
|
||||
performance.clearMeasures();
|
||||
}
|
||||
}
|
||||
|
||||
export const startupProfiler = StartupProfiler.getInstance();
|
||||
|
||||
@@ -14,10 +14,15 @@ const installationManager = new InstallationManager();
|
||||
|
||||
export function getCommonAttributes(config: Config): Attributes {
|
||||
const email = userAccountManager.getCachedGoogleAccount();
|
||||
const experiments = config.getExperiments();
|
||||
return {
|
||||
'session.id': config.getSessionId(),
|
||||
'installation.id': installationManager.getInstallationId(),
|
||||
interactive: config.isInteractive(),
|
||||
...(email && { 'user.email': email }),
|
||||
...(experiments &&
|
||||
experiments.experimentIds.length > 0 && {
|
||||
'experiments.ids': experiments.experimentIds,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -901,9 +901,9 @@ describe('mcp-client', () => {
|
||||
vi.mocked(ClientLib.Client).mockReturnValue(
|
||||
mockedClient as unknown as ClientLib.Client,
|
||||
);
|
||||
vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({
|
||||
close: vi.fn(),
|
||||
} as unknown as SdkClientStdioLib.StdioClientTransport);
|
||||
vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue(
|
||||
{} as SdkClientStdioLib.StdioClientTransport,
|
||||
);
|
||||
const mockedToolRegistry = {
|
||||
registerTool: vi.fn(),
|
||||
unregisterTool: vi.fn(),
|
||||
@@ -1623,7 +1623,7 @@ describe('mcp-client', () => {
|
||||
{
|
||||
command: 'test-command',
|
||||
args: ['--foo', 'bar'],
|
||||
env: { GEMINI_CLI_FOO: 'bar' },
|
||||
env: { FOO: 'bar' },
|
||||
cwd: 'test/cwd',
|
||||
},
|
||||
false,
|
||||
@@ -1634,80 +1634,11 @@ describe('mcp-client', () => {
|
||||
command: 'test-command',
|
||||
args: ['--foo', 'bar'],
|
||||
cwd: 'test/cwd',
|
||||
env: expect.objectContaining({ GEMINI_CLI_FOO: 'bar' }),
|
||||
env: expect.objectContaining({ FOO: 'bar' }),
|
||||
stderr: 'pipe',
|
||||
});
|
||||
});
|
||||
|
||||
it('should redact sensitive environment variables for command transport', async () => {
|
||||
const mockedTransport = vi
|
||||
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
|
||||
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
|
||||
|
||||
const originalEnv = process.env;
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
GEMINI_API_KEY: 'sensitive-key',
|
||||
GEMINI_CLI_SAFE_VAR: 'safe-value',
|
||||
};
|
||||
// Ensure strict sanitization is not triggered for this test
|
||||
delete process.env['GITHUB_SHA'];
|
||||
delete process.env['SURFACE'];
|
||||
|
||||
try {
|
||||
await createTransport(
|
||||
'test-server',
|
||||
{
|
||||
command: 'test-command',
|
||||
},
|
||||
false,
|
||||
EMPTY_CONFIG,
|
||||
);
|
||||
|
||||
const callArgs = mockedTransport.mock.calls[0][0];
|
||||
expect(callArgs.env).toBeDefined();
|
||||
expect(callArgs.env!['GEMINI_CLI_SAFE_VAR']).toBe('safe-value');
|
||||
expect(callArgs.env!['GEMINI_API_KEY']).toBeUndefined();
|
||||
} finally {
|
||||
process.env = originalEnv;
|
||||
}
|
||||
});
|
||||
|
||||
it('should include extension settings in environment', async () => {
|
||||
const mockedTransport = vi
|
||||
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
|
||||
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
|
||||
|
||||
await createTransport(
|
||||
'test-server',
|
||||
{
|
||||
command: 'test-command',
|
||||
extension: {
|
||||
name: 'test-ext',
|
||||
resolvedSettings: [
|
||||
{
|
||||
envVar: 'GEMINI_CLI_EXT_VAR',
|
||||
value: 'ext-value',
|
||||
sensitive: false,
|
||||
name: 'ext-setting',
|
||||
},
|
||||
],
|
||||
version: '',
|
||||
isActive: false,
|
||||
path: '',
|
||||
contextFiles: [],
|
||||
id: '',
|
||||
},
|
||||
},
|
||||
false,
|
||||
EMPTY_CONFIG,
|
||||
);
|
||||
|
||||
const callArgs = mockedTransport.mock.calls[0][0];
|
||||
expect(callArgs.env).toBeDefined();
|
||||
expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBe('ext-value');
|
||||
});
|
||||
|
||||
it('should exclude extension settings with undefined values from environment', async () => {
|
||||
const mockedTransport = vi
|
||||
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
|
||||
@@ -2040,7 +1971,7 @@ describe('connectToMcpServer with OAuth', () => {
|
||||
EMPTY_CONFIG,
|
||||
);
|
||||
|
||||
expect(client.client).toBe(mockedClient);
|
||||
expect(client).toBe(mockedClient);
|
||||
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
||||
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -2086,7 +2017,7 @@ describe('connectToMcpServer with OAuth', () => {
|
||||
EMPTY_CONFIG,
|
||||
);
|
||||
|
||||
expect(client.client).toBe(mockedClient);
|
||||
expect(client).toBe(mockedClient);
|
||||
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
||||
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
|
||||
expect(OAuthUtils.discoverOAuthConfig).toHaveBeenCalledWith(serverUrl);
|
||||
@@ -2181,7 +2112,7 @@ describe('connectToMcpServer - HTTP→SSE fallback', () => {
|
||||
EMPTY_CONFIG,
|
||||
);
|
||||
|
||||
expect(client.client).toBe(mockedClient);
|
||||
expect(client).toBe(mockedClient);
|
||||
// First HTTP attempt fails, second SSE attempt succeeds
|
||||
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -2222,7 +2153,7 @@ describe('connectToMcpServer - HTTP→SSE fallback', () => {
|
||||
EMPTY_CONFIG,
|
||||
);
|
||||
|
||||
expect(client.client).toBe(mockedClient);
|
||||
expect(client).toBe(mockedClient);
|
||||
expect(mockedClient.connect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -2307,7 +2238,7 @@ describe('connectToMcpServer - OAuth with transport fallback', () => {
|
||||
EMPTY_CONFIG,
|
||||
);
|
||||
|
||||
expect(client.client).toBe(mockedClient);
|
||||
expect(client).toBe(mockedClient);
|
||||
expect(mockedClient.connect).toHaveBeenCalledTimes(3);
|
||||
expect(mockAuthProvider.authenticate).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -34,11 +34,7 @@ import {
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { ApprovalMode, PolicyDecision } from '../policy/types.js';
|
||||
import { parse } from 'shell-quote';
|
||||
import type {
|
||||
Config,
|
||||
GeminiCLIExtension,
|
||||
MCPServerConfig,
|
||||
} from '../config/config.js';
|
||||
import type { Config, MCPServerConfig } from '../config/config.js';
|
||||
import { AuthProviderType } from '../config/config.js';
|
||||
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
|
||||
import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js';
|
||||
@@ -146,7 +142,7 @@ export class McpClient {
|
||||
}
|
||||
this.updateStatus(MCPServerStatus.CONNECTING);
|
||||
try {
|
||||
const { client, transport } = await connectToMcpServer(
|
||||
this.client = await connectToMcpServer(
|
||||
this.clientVersion,
|
||||
this.serverName,
|
||||
this.serverConfig,
|
||||
@@ -154,13 +150,11 @@ export class McpClient {
|
||||
this.workspaceContext,
|
||||
this.cliConfig.sanitizationConfig,
|
||||
);
|
||||
this.client = client;
|
||||
this.transport = transport;
|
||||
|
||||
this.registerNotificationHandlers();
|
||||
|
||||
const originalOnError = this.client.onerror;
|
||||
this.client.onerror = async (error) => {
|
||||
this.client.onerror = (error) => {
|
||||
if (this.status !== MCPServerStatus.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
@@ -171,14 +165,6 @@ export class McpClient {
|
||||
error,
|
||||
);
|
||||
this.updateStatus(MCPServerStatus.DISCONNECTED);
|
||||
// Close transport to prevent memory leaks
|
||||
if (this.transport) {
|
||||
try {
|
||||
await this.transport.close();
|
||||
} catch {
|
||||
// Ignore errors when closing transport on error
|
||||
}
|
||||
}
|
||||
};
|
||||
this.updateStatus(MCPServerStatus.CONNECTED);
|
||||
} catch (error) {
|
||||
@@ -927,9 +913,8 @@ export async function connectAndDiscover(
|
||||
updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTING);
|
||||
|
||||
let mcpClient: Client | undefined;
|
||||
let transport: Transport | undefined;
|
||||
try {
|
||||
const result = await connectToMcpServer(
|
||||
mcpClient = await connectToMcpServer(
|
||||
clientVersion,
|
||||
mcpServerName,
|
||||
mcpServerConfig,
|
||||
@@ -937,20 +922,10 @@ export async function connectAndDiscover(
|
||||
workspaceContext,
|
||||
cliConfig.sanitizationConfig,
|
||||
);
|
||||
mcpClient = result.client;
|
||||
transport = result.transport;
|
||||
|
||||
mcpClient.onerror = async (error) => {
|
||||
mcpClient.onerror = (error) => {
|
||||
coreEvents.emitFeedback('error', `MCP ERROR (${mcpServerName}):`, error);
|
||||
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
|
||||
// Close transport to prevent memory leaks
|
||||
if (transport) {
|
||||
try {
|
||||
await transport.close();
|
||||
} catch {
|
||||
// Ignore errors when closing transport on error
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Attempt to discover both prompts and tools
|
||||
@@ -1348,18 +1323,16 @@ function createSSETransportWithAuth(
|
||||
* @param client The MCP client to connect
|
||||
* @param config The MCP server configuration
|
||||
* @param accessToken Optional OAuth access token for authentication
|
||||
* @returns The transport used for connection
|
||||
*/
|
||||
async function connectWithSSETransport(
|
||||
client: Client,
|
||||
config: MCPServerConfig,
|
||||
accessToken?: string | null,
|
||||
): Promise<Transport> {
|
||||
): Promise<void> {
|
||||
const transport = createSSETransportWithAuth(config, accessToken);
|
||||
await client.connect(transport, {
|
||||
timeout: config.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
||||
});
|
||||
return transport;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1389,7 +1362,6 @@ async function showAuthRequiredMessage(serverName: string): Promise<never> {
|
||||
* @param config The MCP server configuration
|
||||
* @param accessToken The OAuth access token to use
|
||||
* @param httpReturned404 Whether the HTTP transport returned 404 (indicating SSE-only server)
|
||||
* @returns The transport used for connection
|
||||
*/
|
||||
async function retryWithOAuth(
|
||||
client: Client,
|
||||
@@ -1397,21 +1369,17 @@ async function retryWithOAuth(
|
||||
config: MCPServerConfig,
|
||||
accessToken: string,
|
||||
httpReturned404: boolean,
|
||||
): Promise<Transport> {
|
||||
): Promise<void> {
|
||||
if (httpReturned404) {
|
||||
// HTTP returned 404, only try SSE
|
||||
debugLogger.log(
|
||||
`Retrying SSE connection to '${serverName}' with OAuth token...`,
|
||||
);
|
||||
const transport = await connectWithSSETransport(
|
||||
client,
|
||||
config,
|
||||
accessToken,
|
||||
);
|
||||
await connectWithSSETransport(client, config, accessToken);
|
||||
debugLogger.log(
|
||||
`Successfully connected to '${serverName}' using SSE with OAuth.`,
|
||||
);
|
||||
return transport;
|
||||
return;
|
||||
}
|
||||
|
||||
// HTTP returned 401, try HTTP with OAuth first
|
||||
@@ -1435,7 +1403,6 @@ async function retryWithOAuth(
|
||||
debugLogger.log(
|
||||
`Successfully connected to '${serverName}' using HTTP with OAuth.`,
|
||||
);
|
||||
return httpTransport;
|
||||
} catch (httpError) {
|
||||
await httpTransport.close();
|
||||
|
||||
@@ -1447,15 +1414,10 @@ async function retryWithOAuth(
|
||||
!config.httpUrl
|
||||
) {
|
||||
debugLogger.log(`HTTP with OAuth returned 404, trying SSE with OAuth...`);
|
||||
const sseTransport = await connectWithSSETransport(
|
||||
client,
|
||||
config,
|
||||
accessToken,
|
||||
);
|
||||
await connectWithSSETransport(client, config, accessToken);
|
||||
debugLogger.log(
|
||||
`Successfully connected to '${serverName}' using SSE with OAuth.`,
|
||||
);
|
||||
return sseTransport;
|
||||
} else {
|
||||
throw httpError;
|
||||
}
|
||||
@@ -1469,7 +1431,7 @@ async function retryWithOAuth(
|
||||
*
|
||||
* @param mcpServerName The name of the MCP server, used for logging and identification.
|
||||
* @param mcpServerConfig The configuration specifying how to connect to the server.
|
||||
* @returns A promise that resolves to a connected MCP `Client` instance and its transport.
|
||||
* @returns A promise that resolves to a connected MCP `Client` instance.
|
||||
* @throws An error if the connection fails or the configuration is invalid.
|
||||
*/
|
||||
export async function connectToMcpServer(
|
||||
@@ -1479,7 +1441,7 @@ export async function connectToMcpServer(
|
||||
debugMode: boolean,
|
||||
workspaceContext: WorkspaceContext,
|
||||
sanitizationConfig: EnvironmentSanitizationConfig,
|
||||
): Promise<{ client: Client; transport: Transport }> {
|
||||
): Promise<Client> {
|
||||
const mcpClient = new Client(
|
||||
{
|
||||
name: 'gemini-cli-mcp-client',
|
||||
@@ -1551,7 +1513,7 @@ export async function connectToMcpServer(
|
||||
await mcpClient.connect(transport, {
|
||||
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
||||
});
|
||||
return { client: mcpClient, transport };
|
||||
return mcpClient;
|
||||
} catch (error) {
|
||||
await transport.close();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
@@ -1583,7 +1545,7 @@ export async function connectToMcpServer(
|
||||
try {
|
||||
// Try SSE with stored OAuth token if available
|
||||
// This ensures that SSE fallback works for authenticated servers
|
||||
const sseTransport = await connectWithSSETransport(
|
||||
await connectWithSSETransport(
|
||||
mcpClient,
|
||||
mcpServerConfig,
|
||||
await getStoredOAuthToken(mcpServerName),
|
||||
@@ -1592,7 +1554,7 @@ export async function connectToMcpServer(
|
||||
debugLogger.log(
|
||||
`MCP server '${mcpServerName}': Successfully connected using SSE transport.`,
|
||||
);
|
||||
return { client: mcpClient, transport: sseTransport };
|
||||
return mcpClient;
|
||||
} catch (sseFallbackError) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
sseError = sseFallbackError as Error;
|
||||
@@ -1700,14 +1662,14 @@ export async function connectToMcpServer(
|
||||
);
|
||||
}
|
||||
|
||||
const oauthTransport = await retryWithOAuth(
|
||||
await retryWithOAuth(
|
||||
mcpClient,
|
||||
mcpServerName,
|
||||
mcpServerConfig,
|
||||
accessToken,
|
||||
httpReturned404,
|
||||
);
|
||||
return { client: mcpClient, transport: oauthTransport };
|
||||
return mcpClient;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to handle automatic OAuth for server '${mcpServerName}'`,
|
||||
@@ -1788,7 +1750,7 @@ export async function connectToMcpServer(
|
||||
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
||||
});
|
||||
// Connection successful with OAuth
|
||||
return { client: mcpClient, transport: oauthTransport };
|
||||
return mcpClient;
|
||||
} else {
|
||||
throw new Error(
|
||||
`OAuth configuration failed for '${mcpServerName}'. Please authenticate manually with /mcp auth ${mcpServerName}`,
|
||||
@@ -1936,23 +1898,10 @@ export async function createTransport(
|
||||
command: mcpServerConfig.command,
|
||||
args: mcpServerConfig.args || [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
env: sanitizeEnvironment(
|
||||
{
|
||||
...process.env,
|
||||
...getExtensionEnvironment(mcpServerConfig.extension),
|
||||
...(mcpServerConfig.env || {}),
|
||||
},
|
||||
{
|
||||
...sanitizationConfig,
|
||||
allowedEnvironmentVariables: [
|
||||
...(sanitizationConfig.allowedEnvironmentVariables ?? []),
|
||||
...(mcpServerConfig.extension?.resolvedSettings?.map(
|
||||
(s) => s.envVar,
|
||||
) ?? []),
|
||||
],
|
||||
enableEnvironmentVariableRedaction: true,
|
||||
},
|
||||
) as Record<string, string>,
|
||||
env: {
|
||||
...sanitizeEnvironment(process.env, sanitizationConfig),
|
||||
...(mcpServerConfig.env || {}),
|
||||
} as Record<string, string>,
|
||||
cwd: mcpServerConfig.cwd,
|
||||
stderr: 'pipe',
|
||||
});
|
||||
@@ -2027,17 +1976,3 @@ export function isEnabled(
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getExtensionEnvironment(
|
||||
extension?: GeminiCLIExtension,
|
||||
): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
if (extension?.resolvedSettings) {
|
||||
for (const setting of extension.resolvedSettings) {
|
||||
if (setting.value) {
|
||||
env[setting.envVar] = setting.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
@@ -1408,42 +1408,45 @@ describe('RipGrepTool', () => {
|
||||
expect(result.llmContent).toContain('L1: HELLO world');
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'fixed_strings parameter',
|
||||
params: { pattern: 'hello.world', fixed_strings: true },
|
||||
mockOutput: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'hello.world\n' },
|
||||
},
|
||||
expectedArgs: ['--fixed-strings'],
|
||||
expectedPattern: 'hello.world',
|
||||
},
|
||||
])(
|
||||
'should handle $name',
|
||||
async ({ params, mockOutput, expectedArgs, expectedPattern }) => {
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData:
|
||||
JSON.stringify({ type: 'match', data: mockOutput }) + '\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
it('should handle fixed_strings parameter', async () => {
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData:
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'hello.world\n' },
|
||||
},
|
||||
}) + '\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const invocation = grepTool.build(params);
|
||||
const result = await invocation.execute(abortSignal);
|
||||
const invocation = grepTool.build({
|
||||
pattern: 'hello.world',
|
||||
fixed_strings: true,
|
||||
});
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(mockSpawn).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining(expectedArgs),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(result.llmContent).toContain(
|
||||
`Found 1 match for pattern "${expectedPattern}"`,
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(mockSpawn).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.arrayContaining(['--fixed-strings']),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(result.llmContent).toContain(
|
||||
'Found 1 match for pattern "hello.world"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow invalid regex patterns when fixed_strings is true', () => {
|
||||
const params: RipGrepToolParams = {
|
||||
pattern: '[[',
|
||||
fixed_strings: true,
|
||||
};
|
||||
expect(grepTool.validateToolParams(params)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle no_ignore parameter', async () => {
|
||||
mockSpawn.mockImplementationOnce(
|
||||
@@ -1681,19 +1684,42 @@ describe('RipGrepTool', () => {
|
||||
mockSpawn.mockImplementationOnce(
|
||||
createMockSpawn({
|
||||
outputData:
|
||||
JSON.stringify({
|
||||
type: 'context',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 1,
|
||||
lines: { text: 'hello world\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'match',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 2,
|
||||
lines: { text: 'second line with world\n' },
|
||||
lines_before: [{ text: 'hello world\n' }],
|
||||
lines_after: [
|
||||
{ text: 'third line\n' },
|
||||
{ text: 'fourth line\n' },
|
||||
],
|
||||
},
|
||||
}) + '\n',
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'context',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 3,
|
||||
lines: { text: 'third line\n' },
|
||||
},
|
||||
}) +
|
||||
'\n' +
|
||||
JSON.stringify({
|
||||
type: 'context',
|
||||
data: {
|
||||
path: { text: 'fileA.txt' },
|
||||
line_number: 4,
|
||||
lines: { text: 'fourth line\n' },
|
||||
},
|
||||
}) +
|
||||
'\n',
|
||||
exitCode: 0,
|
||||
}),
|
||||
);
|
||||
@@ -1721,9 +1747,10 @@ describe('RipGrepTool', () => {
|
||||
);
|
||||
expect(result.llmContent).toContain('Found 1 match for pattern "world"');
|
||||
expect(result.llmContent).toContain('File: fileA.txt');
|
||||
expect(result.llmContent).toContain('L1- hello world');
|
||||
expect(result.llmContent).toContain('L2: second line with world');
|
||||
// Note: Ripgrep JSON output for context lines doesn't include line numbers for context lines directly
|
||||
// The current parsing only extracts the matched line, so we only assert on that.
|
||||
expect(result.llmContent).toContain('L3- third line');
|
||||
expect(result.llmContent).toContain('L4- fourth line');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user