Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/cli/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ Settings are organized into categories. All settings should be placed within the
- It must return function output as JSON on `stdout`, analogous to [`functionResponse.response.content`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functionresponse).
- **Default:** `undefined`

- **`tools.useRipgrep`** (boolean):
- **Description:** Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.
- **Default:** `true`

- **`tools.useBuiltinRipgrep`** (boolean):
- **Description:** Use the bundled ripgrep binary. When set to `false`, the system-level `rg` command will be used instead. This setting is only effective when `tools.useRipgrep` is `true`.
- **Default:** `true`

#### `mcp`

- **`mcp.serverCommand`** (string):
Expand Down
67 changes: 67 additions & 0 deletions packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2399,6 +2399,73 @@ describe('loadCliConfig useRipgrep', () => {
});
});

describe('loadCliConfig useBuiltinRipgrep', () => {
const originalArgv = process.argv;

beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
});

afterEach(() => {
process.argv = originalArgv;
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

it('should be true by default when useBuiltinRipgrep is not set in settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {};
const config = await loadCliConfig(
settings,
[],
new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(),
argv.extensions,
),
'test-session',
argv,
);
expect(config.getUseBuiltinRipgrep()).toBe(true);
});

it('should be false when useBuiltinRipgrep is set to false in settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { tools: { useBuiltinRipgrep: false } };
const config = await loadCliConfig(
settings,
[],
new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(),
argv.extensions,
),
'test-session',
argv,
);
expect(config.getUseBuiltinRipgrep()).toBe(false);
});

it('should be true when useBuiltinRipgrep is explicitly set to true in settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { tools: { useBuiltinRipgrep: true } };
const config = await loadCliConfig(
settings,
[],
new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(),
argv.extensions,
),
'test-session',
argv,
);
expect(config.getUseBuiltinRipgrep()).toBe(true);
});
});

describe('screenReader configuration', () => {
const originalArgv = process.argv;

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
})
.option('proxy', {
type: 'string',
description:
'Proxy for Qwen Code, like schema://user:password@host:port',
description: 'Proxy for Qwen Code, like schema://user:password@host:port',
})
.deprecateOption(
'proxy',
Expand Down Expand Up @@ -761,6 +760,7 @@ export async function loadCliConfig(
interactive,
trustedFolder,
useRipgrep: settings.tools?.useRipgrep,
useBuiltinRipgrep: settings.tools?.useBuiltinRipgrep,
shouldUseNodePtyShell: settings.tools?.shell?.enableInteractiveShell,
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
enablePromptCompletion: settings.general?.enablePromptCompletion ?? false,
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,16 @@ const SETTINGS_SCHEMA = {
'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.',
showInDialog: true,
},
useBuiltinRipgrep: {
type: 'boolean',
label: 'Use Builtin Ripgrep',
category: 'Tools',
requiresRestart: false,
default: true,
description:
'Use the bundled ripgrep binary. When set to false, the system-level "rg" command will be used instead. This setting is only effective when useRipgrep is true.',
showInDialog: true,
},
enableToolOutputTruncation: {
type: 'boolean',
label: 'Enable Tool Output Truncation',
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,11 @@ export async function main() {
let input = config.getQuestion();
const startupWarnings = [
...(await getStartupWarnings()),
...(await getUserStartupWarnings()),
...(await getUserStartupWarnings({
workspaceRoot: process.cwd(),
useRipgrep: settings.merged.tools?.useRipgrep ?? true,
useBuiltinRipgrep: settings.merged.tools?.useBuiltinRipgrep ?? true,
})),
];

// Render UI, passing necessary config values. Check that there is no command line question.
Expand Down
35 changes: 30 additions & 5 deletions packages/cli/src/utils/userStartupWarnings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,22 @@ vi.mock('os', async (importOriginal) => {
describe('getUserStartupWarnings', () => {
let testRootDir: string;
let homeDir: string;
let startupOptions: {
workspaceRoot: string;
useRipgrep: boolean;
useBuiltinRipgrep: boolean;
};

beforeEach(async () => {
testRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'warnings-test-'));
homeDir = path.join(testRootDir, 'home');
await fs.mkdir(homeDir, { recursive: true });
vi.mocked(os.homedir).mockReturnValue(homeDir);
startupOptions = {
workspaceRoot: testRootDir,
useRipgrep: true,
useBuiltinRipgrep: true,
};
});

afterEach(async () => {
Expand All @@ -37,7 +47,10 @@ describe('getUserStartupWarnings', () => {

describe('home directory check', () => {
it('should return a warning when running in home directory', async () => {
const warnings = await getUserStartupWarnings(homeDir);
const warnings = await getUserStartupWarnings({
...startupOptions,
workspaceRoot: homeDir,
});
expect(warnings).toContainEqual(
expect.stringContaining('home directory'),
);
Expand All @@ -46,7 +59,10 @@ describe('getUserStartupWarnings', () => {
it('should not return a warning when running in a project directory', async () => {
const projectDir = path.join(testRootDir, 'project');
await fs.mkdir(projectDir);
const warnings = await getUserStartupWarnings(projectDir);
const warnings = await getUserStartupWarnings({
...startupOptions,
workspaceRoot: projectDir,
});
expect(warnings).not.toContainEqual(
expect.stringContaining('home directory'),
);
Expand All @@ -56,7 +72,10 @@ describe('getUserStartupWarnings', () => {
describe('root directory check', () => {
it('should return a warning when running in a root directory', async () => {
const rootDir = path.parse(testRootDir).root;
const warnings = await getUserStartupWarnings(rootDir);
const warnings = await getUserStartupWarnings({
...startupOptions,
workspaceRoot: rootDir,
});
expect(warnings).toContainEqual(
expect.stringContaining('root directory'),
);
Expand All @@ -68,7 +87,10 @@ describe('getUserStartupWarnings', () => {
it('should not return a warning when running in a non-root directory', async () => {
const projectDir = path.join(testRootDir, 'project');
await fs.mkdir(projectDir);
const warnings = await getUserStartupWarnings(projectDir);
const warnings = await getUserStartupWarnings({
...startupOptions,
workspaceRoot: projectDir,
});
expect(warnings).not.toContainEqual(
expect.stringContaining('root directory'),
);
Expand All @@ -78,7 +100,10 @@ describe('getUserStartupWarnings', () => {
describe('error handling', () => {
it('should handle errors when checking directory', async () => {
const nonExistentPath = path.join(testRootDir, 'non-existent');
const warnings = await getUserStartupWarnings(nonExistentPath);
const warnings = await getUserStartupWarnings({
...startupOptions,
workspaceRoot: nonExistentPath,
});
const expectedWarning =
'Could not verify the current directory due to a file system error.';
expect(warnings).toEqual([expectedWarning, expectedWarning]);
Expand Down
37 changes: 30 additions & 7 deletions packages/cli/src/utils/userStartupWarnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@
import fs from 'node:fs/promises';
import * as os from 'node:os';
import path from 'node:path';
import { canUseRipgrep } from '@qwen-code/qwen-code-core';

type WarningCheckOptions = {
workspaceRoot: string;
useRipgrep: boolean;
useBuiltinRipgrep: boolean;
};

type WarningCheck = {
id: string;
check: (workspaceRoot: string) => Promise<string | null>;
check: (options: WarningCheckOptions) => Promise<string | null>;
};

// Individual warning checks
const homeDirectoryCheck: WarningCheck = {
id: 'home-directory',
check: async (workspaceRoot: string) => {
check: async (options: WarningCheckOptions) => {
try {
const [workspaceRealPath, homeRealPath] = await Promise.all([
fs.realpath(workspaceRoot),
fs.realpath(options.workspaceRoot),
fs.realpath(os.homedir()),
]);

Expand All @@ -35,9 +42,9 @@ const homeDirectoryCheck: WarningCheck = {

const rootDirectoryCheck: WarningCheck = {
id: 'root-directory',
check: async (workspaceRoot: string) => {
check: async (options: WarningCheckOptions) => {
try {
const workspaceRealPath = await fs.realpath(workspaceRoot);
const workspaceRealPath = await fs.realpath(options.workspaceRoot);
const errorMessage =
'Warning: You are running Qwen Code in the root directory. Your entire folder structure will be used for context. It is strongly recommended to run in a project-specific directory.';

Expand All @@ -53,17 +60,33 @@ const rootDirectoryCheck: WarningCheck = {
},
};

const ripgrepAvailabilityCheck: WarningCheck = {
id: 'ripgrep-availability',
check: async (options: WarningCheckOptions) => {
if (!options.useRipgrep) {
return null;
}

const isAvailable = await canUseRipgrep(options.useBuiltinRipgrep);
if (!isAvailable) {
return 'Ripgrep not available: Please install ripgrep globally to enable faster file content search. Falling back to built-in grep.';
}
return null;
},
};

// All warning checks
const WARNING_CHECKS: readonly WarningCheck[] = [
homeDirectoryCheck,
rootDirectoryCheck,
ripgrepAvailabilityCheck,
];

export async function getUserStartupWarnings(
workspaceRoot: string = process.cwd(),
options: WarningCheckOptions,
): Promise<string[]> {
const results = await Promise.all(
WARNING_CHECKS.map((check) => check.check(workspaceRoot)),
WARNING_CHECKS.map((check) => check.check(options)),
);
return results.filter((msg) => msg !== null);
}
Loading