mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 12:40:44 +00:00
Merge branch 'main' into pr-1539
This commit is contained in:
commit
1c5b74ebd9
210 changed files with 22365 additions and 2747 deletions
|
|
@ -168,7 +168,7 @@ describe('validateAuthMethod', () => {
|
|||
expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();
|
||||
});
|
||||
|
||||
it('should use config.modelsConfig.getModel() when Config is provided', () => {
|
||||
it('should use config.getModelsConfig().getModel() when Config is provided', () => {
|
||||
// Settings has a different model
|
||||
vi.mocked(settings.loadSettings).mockReturnValue({
|
||||
merged: {
|
||||
|
|
@ -184,18 +184,18 @@ describe('validateAuthMethod', () => {
|
|||
|
||||
// Mock Config object that returns a different model (e.g., from CLI args)
|
||||
const mockConfig = {
|
||||
modelsConfig: {
|
||||
getModelsConfig: vi.fn().mockReturnValue({
|
||||
getModel: vi.fn().mockReturnValue('cli-model'),
|
||||
},
|
||||
}),
|
||||
} as unknown as import('@qwen-code/qwen-code-core').Config;
|
||||
|
||||
// Set the env key for the CLI model, not the settings model
|
||||
process.env['CLI_API_KEY'] = 'cli-key';
|
||||
|
||||
// Should use 'cli-model' from config.modelsConfig.getModel(), not 'settings-model'
|
||||
// Should use 'cli-model' from config.getModelsConfig().getModel(), not 'settings-model'
|
||||
const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);
|
||||
expect(result).toBeNull();
|
||||
expect(mockConfig.modelsConfig.getModel).toHaveBeenCalled();
|
||||
expect(mockConfig.getModelsConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail validation when Config provides different model without matching env key', () => {
|
||||
|
|
@ -217,9 +217,9 @@ describe('validateAuthMethod', () => {
|
|||
} as unknown as ReturnType<typeof settings.loadSettings>);
|
||||
|
||||
const mockConfig = {
|
||||
modelsConfig: {
|
||||
getModelsConfig: vi.fn().mockReturnValue({
|
||||
getModel: vi.fn().mockReturnValue('cli-model'),
|
||||
},
|
||||
}),
|
||||
} as unknown as import('@qwen-code/qwen-code-core').Config;
|
||||
|
||||
// Don't set CLI_API_KEY - validation should fail
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ function hasApiKeyForAuth(
|
|||
| ModelProvidersConfig
|
||||
| undefined;
|
||||
|
||||
// Use config.modelsConfig.getModel() if available for accurate model ID resolution
|
||||
// Use config.getModelsConfig().getModel() if available for accurate model ID resolution
|
||||
// that accounts for CLI args, env vars, and settings. Fall back to settings.model.name.
|
||||
const modelId = config?.modelsConfig.getModel() ?? settings.model?.name;
|
||||
const modelId = config?.getModelsConfig().getModel() ?? settings.model?.name;
|
||||
|
||||
// Try to find model-specific envKey from modelProviders
|
||||
const modelConfig = findModelConfig(modelProviders, authType, modelId);
|
||||
|
|
@ -184,9 +184,9 @@ export function validateAuthMethod(
|
|||
const modelProviders = settings.merged.modelProviders as
|
||||
| ModelProvidersConfig
|
||||
| undefined;
|
||||
// Use config.modelsConfig.getModel() if available for accurate model ID
|
||||
// Use config.getModelsConfig().getModel() if available for accurate model ID
|
||||
const modelId =
|
||||
config?.modelsConfig.getModel() ?? settings.merged.model?.name;
|
||||
config?.getModelsConfig().getModel() ?? settings.merged.model?.name;
|
||||
const modelConfig = findModelConfig(modelProviders, authMethod, modelId);
|
||||
|
||||
if (modelConfig && !modelConfig.baseUrl) {
|
||||
|
|
|
|||
|
|
@ -13,18 +13,48 @@ import {
|
|||
WriteFileTool,
|
||||
DEFAULT_QWEN_MODEL,
|
||||
OutputFormat,
|
||||
NativeLspService,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
||||
import type { Settings } from './settings.js';
|
||||
import * as ServerConfig from '@qwen-code/qwen-code-core';
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
|
||||
const createNativeLspServiceInstance = () => ({
|
||||
discoverAndPrepare: vi.fn(),
|
||||
start: vi.fn(),
|
||||
definitions: vi.fn().mockResolvedValue([]),
|
||||
references: vi.fn().mockResolvedValue([]),
|
||||
workspaceSymbols: vi.fn().mockResolvedValue([]),
|
||||
hover: vi.fn().mockResolvedValue(null),
|
||||
documentSymbols: vi.fn().mockResolvedValue([]),
|
||||
implementations: vi.fn().mockResolvedValue([]),
|
||||
prepareCallHierarchy: vi.fn().mockResolvedValue([]),
|
||||
incomingCalls: vi.fn().mockResolvedValue([]),
|
||||
outgoingCalls: vi.fn().mockResolvedValue([]),
|
||||
diagnostics: vi.fn().mockResolvedValue([]),
|
||||
workspaceDiagnostics: vi.fn().mockResolvedValue([]),
|
||||
codeActions: vi.fn().mockResolvedValue([]),
|
||||
applyWorkspaceEdit: vi.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
||||
vi.mock('./trustedFolders.js', () => ({
|
||||
isWorkspaceTrusted: vi
|
||||
.fn()
|
||||
.mockReturnValue({ isTrusted: true, source: 'file' }), // Default to trusted
|
||||
}));
|
||||
|
||||
const nativeLspServiceMock = vi.mocked(NativeLspService);
|
||||
const getLastLspInstance = () => {
|
||||
const results = nativeLspServiceMock.mock.results;
|
||||
if (results.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return results[results.length - 1]?.value as ReturnType<
|
||||
typeof createNativeLspServiceInstance
|
||||
>;
|
||||
};
|
||||
|
||||
vi.mock('fs', async (importOriginal) => {
|
||||
const actualFs = await importOriginal<typeof import('fs')>();
|
||||
const pathMod = await import('node:path');
|
||||
|
|
@ -79,6 +109,9 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
|||
const actualServer = await importOriginal<typeof ServerConfig>();
|
||||
return {
|
||||
...actualServer,
|
||||
NativeLspService: vi
|
||||
.fn()
|
||||
.mockImplementation(() => createNativeLspServiceInstance()),
|
||||
IdeClient: {
|
||||
getInstance: vi.fn().mockResolvedValue({
|
||||
getConnectionStatus: vi.fn(),
|
||||
|
|
@ -514,6 +547,10 @@ describe('loadCliConfig', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
nativeLspServiceMock.mockReset();
|
||||
nativeLspServiceMock.mockImplementation(
|
||||
() => createNativeLspServiceInstance() as unknown as NativeLspService,
|
||||
);
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
});
|
||||
|
|
@ -543,6 +580,22 @@ describe('loadCliConfig', () => {
|
|||
expect(config.getIncludePartialMessages()).toBe(true);
|
||||
});
|
||||
|
||||
it('should initialize native LSP service when enabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--experimental-lsp'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
|
||||
const config = await loadCliConfig(settings, argv);
|
||||
|
||||
// LSP is enabled via --experimental-lsp flag
|
||||
expect(config.isLspEnabled()).toBe(true);
|
||||
expect(nativeLspServiceMock).toHaveBeenCalledTimes(1);
|
||||
const lspInstance = getLastLspInstance();
|
||||
expect(lspInstance).toBeDefined();
|
||||
expect(lspInstance?.discoverAndPrepare).toHaveBeenCalledTimes(1);
|
||||
expect(lspInstance?.start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('Proxy configuration', () => {
|
||||
const originalProxyEnv: { [key: string]: string | undefined } = {};
|
||||
const proxyEnvVars = [
|
||||
|
|
|
|||
|
|
@ -20,11 +20,15 @@ import {
|
|||
OutputFormat,
|
||||
isToolEnabled,
|
||||
SessionService,
|
||||
ideContextStore,
|
||||
type ResumedSessionData,
|
||||
type LspClient,
|
||||
type ToolName,
|
||||
EditTool,
|
||||
ShellTool,
|
||||
WriteFileTool,
|
||||
NativeLspClient,
|
||||
NativeLspService,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import type { Settings } from './settings.js';
|
||||
|
|
@ -113,6 +117,7 @@ export interface CliArgs {
|
|||
acp: boolean | undefined;
|
||||
experimentalAcp: boolean | undefined;
|
||||
experimentalSkills: boolean | undefined;
|
||||
experimentalLsp: boolean | undefined;
|
||||
extensions: string[] | undefined;
|
||||
listExtensions: boolean | undefined;
|
||||
openaiLogging: boolean | undefined;
|
||||
|
|
@ -331,6 +336,12 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
|||
return settings.experimental?.skills ?? legacySkills ?? false;
|
||||
})(),
|
||||
})
|
||||
.option('experimental-lsp', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Enable experimental LSP (Language Server Protocol) feature for code intelligence',
|
||||
default: false,
|
||||
})
|
||||
.option('channel', {
|
||||
type: 'string',
|
||||
choices: ['VSCode', 'ACP', 'SDK', 'CI'],
|
||||
|
|
@ -713,6 +724,9 @@ export async function loadCliConfig(
|
|||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
// LSP configuration: enabled only via --experimental-lsp flag
|
||||
const lspEnabled = argv.experimentalLsp === true;
|
||||
let lspClient: LspClient | undefined;
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
const inputFormat: InputFormat =
|
||||
(argv.inputFormat as InputFormat | undefined) ?? InputFormat.TEXT;
|
||||
|
|
@ -924,7 +938,7 @@ export async function loadCliConfig(
|
|||
|
||||
const modelProvidersConfig = settings.modelProviders;
|
||||
|
||||
return new Config({
|
||||
const config = new Config({
|
||||
sessionId,
|
||||
sessionData,
|
||||
embeddingModel: DEFAULT_QWEN_EMBEDDING_MODEL,
|
||||
|
|
@ -932,7 +946,7 @@ export async function loadCliConfig(
|
|||
targetDir: cwd,
|
||||
includeDirectories,
|
||||
loadMemoryFromIncludeDirectories:
|
||||
settings.context?.loadMemoryFromIncludeDirectories || false,
|
||||
settings.context?.loadFromIncludeDirectories || false,
|
||||
importFormat: settings.context?.importFormat || 'tree',
|
||||
debugMode,
|
||||
question,
|
||||
|
|
@ -1016,7 +1030,34 @@ export async function loadCliConfig(
|
|||
// always be true and the settings file can never disable recording.
|
||||
chatRecording:
|
||||
argv.chatRecording ?? settings.general?.chatRecording ?? true,
|
||||
lsp: {
|
||||
enabled: lspEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
if (lspEnabled) {
|
||||
try {
|
||||
const lspService = new NativeLspService(
|
||||
config,
|
||||
config.getWorkspaceContext(),
|
||||
appEvents,
|
||||
fileService,
|
||||
ideContextStore,
|
||||
{
|
||||
requireTrustedWorkspace: folderTrust,
|
||||
},
|
||||
);
|
||||
|
||||
await lspService.discoverAndPrepare();
|
||||
await lspService.start();
|
||||
lspClient = new NativeLspClient(lspService);
|
||||
config.setLspClient(lspClient);
|
||||
} catch (err) {
|
||||
logger.warn('Failed to initialize native LSP service:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function mergeExcludeTools(
|
||||
|
|
|
|||
|
|
@ -218,14 +218,14 @@ describe('SettingsSchema', () => {
|
|||
},
|
||||
context: {
|
||||
includeDirectories: ['/path/to/dir'],
|
||||
loadMemoryFromIncludeDirectories: true,
|
||||
loadFromIncludeDirectories: true,
|
||||
},
|
||||
};
|
||||
|
||||
// TypeScript should not complain about these properties
|
||||
expect(settings.ui?.theme).toBe('dark');
|
||||
expect(settings.context?.includeDirectories).toEqual(['/path/to/dir']);
|
||||
expect(settings.context?.loadMemoryFromIncludeDirectories).toBe(true);
|
||||
expect(settings.context?.loadFromIncludeDirectories).toBe(true);
|
||||
});
|
||||
|
||||
it('should have includeDirectories setting in schema', () => {
|
||||
|
|
@ -243,21 +243,19 @@ describe('SettingsSchema', () => {
|
|||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('should have loadMemoryFromIncludeDirectories setting in schema', () => {
|
||||
it('should have loadFromIncludeDirectories setting in schema', () => {
|
||||
expect(
|
||||
getSettingsSchema().context?.properties
|
||||
.loadMemoryFromIncludeDirectories,
|
||||
getSettingsSchema().context?.properties.loadFromIncludeDirectories,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
getSettingsSchema().context?.properties.loadMemoryFromIncludeDirectories
|
||||
.type,
|
||||
getSettingsSchema().context?.properties.loadFromIncludeDirectories.type,
|
||||
).toBe('boolean');
|
||||
expect(
|
||||
getSettingsSchema().context?.properties.loadMemoryFromIncludeDirectories
|
||||
getSettingsSchema().context?.properties.loadFromIncludeDirectories
|
||||
.category,
|
||||
).toBe('Context');
|
||||
expect(
|
||||
getSettingsSchema().context?.properties.loadMemoryFromIncludeDirectories
|
||||
getSettingsSchema().context?.properties.loadFromIncludeDirectories
|
||||
.default,
|
||||
).toBe(false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import type { CustomTheme } from '../ui/themes/theme.js';
|
||||
import { getLanguageSettingsOptions } from '../i18n/languages.js';
|
||||
|
||||
export type SettingsType =
|
||||
| 'boolean'
|
||||
|
|
@ -210,13 +211,7 @@ const SETTINGS_SCHEMA = {
|
|||
'You can also use custom language codes (e.g., "es", "fr") by placing JS language files ' +
|
||||
'in ~/.qwen/locales/ (e.g., ~/.qwen/locales/es.js).',
|
||||
showInDialog: true,
|
||||
options: [
|
||||
{ value: 'auto', label: 'Auto (detect from system)' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh', label: '中文 (Chinese)' },
|
||||
{ value: 'ru', label: 'Русский (Russian)' },
|
||||
{ value: 'de', label: 'Deutsch (German)' },
|
||||
],
|
||||
options: [] as readonly SettingEnumOption[],
|
||||
},
|
||||
outputLanguage: {
|
||||
type: 'string',
|
||||
|
|
@ -226,7 +221,7 @@ const SETTINGS_SCHEMA = {
|
|||
default: 'auto',
|
||||
description:
|
||||
'The language for LLM output. Use "auto" to detect from system settings, ' +
|
||||
'or set a specific language (e.g., "English", "中文", "日本語").',
|
||||
'or set a specific language.',
|
||||
showInDialog: true,
|
||||
},
|
||||
terminalBell: {
|
||||
|
|
@ -693,7 +688,7 @@ const SETTINGS_SCHEMA = {
|
|||
showInDialog: false,
|
||||
mergeStrategy: MergeStrategy.CONCAT,
|
||||
},
|
||||
loadMemoryFromIncludeDirectories: {
|
||||
loadFromIncludeDirectories: {
|
||||
type: 'boolean',
|
||||
label: 'Load Memory From Include Directories',
|
||||
category: 'Context',
|
||||
|
|
@ -1195,6 +1190,15 @@ const SETTINGS_SCHEMA = {
|
|||
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
|
||||
|
||||
export function getSettingsSchema(): SettingsSchemaType {
|
||||
// Inject dynamic language options
|
||||
const schema = SETTINGS_SCHEMA as unknown as SettingsSchema;
|
||||
if (schema['general']?.properties?.['language']) {
|
||||
(
|
||||
schema['general'].properties['language'] as {
|
||||
options?: SettingEnumOption[];
|
||||
}
|
||||
).options = getLanguageSettingsOptions();
|
||||
}
|
||||
return SETTINGS_SCHEMA;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue