mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
refactor: refactor settings to a nested structure (#7244)
This commit is contained in:
@@ -333,7 +333,7 @@ describe('loadCliConfig', () => {
|
||||
it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { showMemoryUsage: false };
|
||||
const settings: Settings = { ui: { showMemoryUsage: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getShowMemoryUsage()).toBe(false);
|
||||
});
|
||||
@@ -341,7 +341,7 @@ describe('loadCliConfig', () => {
|
||||
it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => {
|
||||
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { showMemoryUsage: false };
|
||||
const settings: Settings = { ui: { showMemoryUsage: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getShowMemoryUsage()).toBe(true);
|
||||
});
|
||||
@@ -804,7 +804,7 @@ describe('mergeExcludeTools', () => {
|
||||
});
|
||||
|
||||
it('should merge excludeTools from settings and extensions', async () => {
|
||||
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
|
||||
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
|
||||
const extensions: Extension[] = [
|
||||
{
|
||||
path: '/path/to/ext1',
|
||||
@@ -840,7 +840,7 @@ describe('mergeExcludeTools', () => {
|
||||
});
|
||||
|
||||
it('should handle overlapping excludeTools between settings and extensions', async () => {
|
||||
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
|
||||
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
|
||||
const extensions: Extension[] = [
|
||||
{
|
||||
path: '/path/to/ext1',
|
||||
@@ -867,7 +867,7 @@ describe('mergeExcludeTools', () => {
|
||||
});
|
||||
|
||||
it('should handle overlapping excludeTools between extensions', async () => {
|
||||
const settings: Settings = { excludeTools: ['tool1'] };
|
||||
const settings: Settings = { tools: { exclude: ['tool1'] } };
|
||||
const extensions: Extension[] = [
|
||||
{
|
||||
path: '/path/to/ext1',
|
||||
@@ -935,7 +935,7 @@ describe('mergeExcludeTools', () => {
|
||||
it('should handle settings with excludeTools but no extensions', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
|
||||
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
|
||||
const extensions: Extension[] = [];
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
@@ -977,7 +977,7 @@ describe('mergeExcludeTools', () => {
|
||||
});
|
||||
|
||||
it('should not modify the original settings object', async () => {
|
||||
const settings: Settings = { excludeTools: ['tool1'] };
|
||||
const settings: Settings = { tools: { exclude: ['tool1'] } };
|
||||
const extensions: Extension[] = [
|
||||
{
|
||||
path: '/path/to/ext',
|
||||
@@ -1166,7 +1166,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
'test',
|
||||
];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { excludeTools: ['custom_tool'] };
|
||||
const settings: Settings = { tools: { exclude: ['custom_tool'] } };
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
const config = await loadCliConfig(
|
||||
@@ -1297,7 +1297,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
...baseSettings,
|
||||
allowMCPServers: ['server1', 'server2'],
|
||||
mcp: { allowed: ['server1', 'server2'] },
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual({
|
||||
@@ -1311,7 +1311,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
...baseSettings,
|
||||
excludeMCPServers: ['server1', 'server2'],
|
||||
mcp: { excluded: ['server1', 'server2'] },
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual({
|
||||
@@ -1324,8 +1324,10 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
...baseSettings,
|
||||
excludeMCPServers: ['server1'],
|
||||
allowMCPServers: ['server1', 'server2'],
|
||||
mcp: {
|
||||
excluded: ['server1'],
|
||||
allowed: ['server1', 'server2'],
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual({
|
||||
@@ -1343,14 +1345,40 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
...baseSettings,
|
||||
excludeMCPServers: ['server1'],
|
||||
allowMCPServers: ['server2'],
|
||||
mcp: {
|
||||
excluded: ['server1'],
|
||||
allowed: ['server2'],
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual({
|
||||
server1: { url: 'http://localhost:8080' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should prioritize CLI flag over both allowed and excluded settings', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
'--allowed-mcp-server-names',
|
||||
'server2',
|
||||
'--allowed-mcp-server-names',
|
||||
'server3',
|
||||
];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
...baseSettings,
|
||||
mcp: {
|
||||
allowed: ['server1', 'server2'], // Should be ignored
|
||||
excluded: ['server3'], // Should be ignored
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getMcpServers()).toEqual({
|
||||
server2: { url: 'http://localhost:8081' },
|
||||
server3: { url: 'http://localhost:8082' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig extensions', () => {
|
||||
@@ -1403,7 +1431,9 @@ describe('loadCliConfig model selection', () => {
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
{
|
||||
model: 'gemini-9001-ultra',
|
||||
model: {
|
||||
name: 'gemini-9001-ultra',
|
||||
},
|
||||
},
|
||||
[],
|
||||
'test-session',
|
||||
@@ -1433,7 +1463,9 @@ describe('loadCliConfig model selection', () => {
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(
|
||||
{
|
||||
model: 'gemini-9001-ultra',
|
||||
model: {
|
||||
name: 'gemini-9001-ultra',
|
||||
},
|
||||
},
|
||||
[],
|
||||
'test-session',
|
||||
@@ -1485,7 +1517,9 @@ describe('loadCliConfig folderTrustFeature', () => {
|
||||
it('should be true when settings.folderTrustFeature is true', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature: true };
|
||||
const settings: Settings = {
|
||||
security: { folderTrust: { featureEnabled: true } },
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrustFeature()).toBe(true);
|
||||
});
|
||||
@@ -1509,8 +1543,12 @@ describe('loadCliConfig folderTrust', () => {
|
||||
it('should be false if folderTrustFeature is false and folderTrust is false', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings: Settings = {
|
||||
folderTrustFeature: false,
|
||||
folderTrust: false,
|
||||
security: {
|
||||
folderTrust: {
|
||||
featureEnabled: false,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
@@ -1520,7 +1558,14 @@ describe('loadCliConfig folderTrust', () => {
|
||||
it('should be false if folderTrustFeature is true and folderTrust is false', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature: true, folderTrust: false };
|
||||
const settings: Settings = {
|
||||
security: {
|
||||
folderTrust: {
|
||||
featureEnabled: true,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrust()).toBe(false);
|
||||
});
|
||||
@@ -1528,7 +1573,14 @@ describe('loadCliConfig folderTrust', () => {
|
||||
it('should be false if folderTrustFeature is false and folderTrust is true', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature: false, folderTrust: true };
|
||||
const settings: Settings = {
|
||||
security: {
|
||||
folderTrust: {
|
||||
featureEnabled: false,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrust()).toBe(false);
|
||||
});
|
||||
@@ -1536,7 +1588,14 @@ describe('loadCliConfig folderTrust', () => {
|
||||
it('should be true when folderTrustFeature is true and folderTrust is true', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature: true, folderTrust: true };
|
||||
const settings: Settings = {
|
||||
security: {
|
||||
folderTrust: {
|
||||
featureEnabled: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getFolderTrust()).toBe(true);
|
||||
});
|
||||
@@ -1570,11 +1629,13 @@ describe('loadCliConfig with includeDirectories', () => {
|
||||
];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
includeDirectories: [
|
||||
path.resolve(path.sep, 'settings', 'path1'),
|
||||
path.join(os.homedir(), 'settings', 'path2'),
|
||||
path.join(mockCwd, 'settings', 'path3'),
|
||||
],
|
||||
context: {
|
||||
includeDirectories: [
|
||||
path.resolve(path.sep, 'settings', 'path1'),
|
||||
path.join(os.homedir(), 'settings', 'path2'),
|
||||
path.join(mockCwd, 'settings', 'path3'),
|
||||
],
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
const expected = [
|
||||
@@ -1613,8 +1674,10 @@ describe('loadCliConfig chatCompression', () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
chatCompression: {
|
||||
contextPercentageThreshold: 0.5,
|
||||
model: {
|
||||
chatCompression: {
|
||||
contextPercentageThreshold: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
@@ -1658,7 +1721,7 @@ describe('loadCliConfig useRipgrep', () => {
|
||||
it('should be true when useRipgrep is set to true in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { useRipgrep: true };
|
||||
const settings: Settings = { tools: { useRipgrep: true } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getUseRipgrep()).toBe(true);
|
||||
});
|
||||
@@ -1666,7 +1729,7 @@ describe('loadCliConfig useRipgrep', () => {
|
||||
it('should be false when useRipgrep is explicitly set to false in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { useRipgrep: false };
|
||||
const settings: Settings = { tools: { useRipgrep: false } };
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getUseRipgrep()).toBe(false);
|
||||
});
|
||||
@@ -2002,13 +2065,26 @@ describe('loadCliConfig trustedFolder', () => {
|
||||
} of testCases) {
|
||||
it(`should be correct for: ${description}`, async () => {
|
||||
(isWorkspaceTrusted as Mock).mockImplementation((settings: Settings) => {
|
||||
const featureIsEnabled =
|
||||
(settings.folderTrustFeature ?? false) &&
|
||||
(settings.folderTrust ?? true);
|
||||
return featureIsEnabled ? mockTrustValue : true;
|
||||
const folderTrustFeature =
|
||||
settings.security?.folderTrust?.featureEnabled ?? false;
|
||||
const folderTrustSetting =
|
||||
settings.security?.folderTrust?.enabled ?? true;
|
||||
const folderTrustEnabled = folderTrustFeature && folderTrustSetting;
|
||||
|
||||
if (!folderTrustEnabled) {
|
||||
return true;
|
||||
}
|
||||
return mockTrustValue; // This is the part that comes from the test case
|
||||
});
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { folderTrustFeature, folderTrust };
|
||||
const settings: Settings = {
|
||||
security: {
|
||||
folderTrust: {
|
||||
featureEnabled: folderTrustFeature,
|
||||
enabled: folderTrust,
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
|
||||
expect(config.getFolderTrust()).toBe(expectedFolderTrust);
|
||||
|
||||
@@ -244,7 +244,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
// Register MCP subcommands
|
||||
.command(mcpCommand);
|
||||
|
||||
if (settings?.extensionManagement ?? false) {
|
||||
if (settings?.experimental?.extensionManagement ?? false) {
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ export async function loadHierarchicalGeminiMemory(
|
||||
extensionContextFilePaths,
|
||||
memoryImportFormat,
|
||||
fileFilteringOptions,
|
||||
settings.memoryDiscoveryMaxDirs,
|
||||
settings.context?.discoveryMaxDirs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -328,12 +328,13 @@ export async function loadCliConfig(
|
||||
(v) => v === 'true' || v === '1',
|
||||
) ||
|
||||
false;
|
||||
const memoryImportFormat = settings.memoryImportFormat || 'tree';
|
||||
const memoryImportFormat = settings.context?.importFormat || 'tree';
|
||||
|
||||
const ideMode = settings.ideMode ?? false;
|
||||
const ideMode = settings.ide?.enabled ?? false;
|
||||
|
||||
const folderTrustFeature = settings.folderTrustFeature ?? false;
|
||||
const folderTrustSetting = settings.folderTrust ?? true;
|
||||
const folderTrustFeature =
|
||||
settings.security?.folderTrust?.featureEnabled ?? false;
|
||||
const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true;
|
||||
const folderTrust = folderTrustFeature && folderTrustSetting;
|
||||
const trustedFolder = isWorkspaceTrusted(settings);
|
||||
|
||||
@@ -351,8 +352,8 @@ export async function loadCliConfig(
|
||||
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
|
||||
// directly to the Config constructor in core, and have core handle setGeminiMdFilename.
|
||||
// However, loadHierarchicalGeminiMemory is called *before* createServerConfig.
|
||||
if (settings.contextFileName) {
|
||||
setServerGeminiMdFilename(settings.contextFileName);
|
||||
if (settings.context?.fileName) {
|
||||
setServerGeminiMdFilename(settings.context.fileName);
|
||||
} else {
|
||||
// Reset to default if not provided in settings.
|
||||
setServerGeminiMdFilename(getCurrentGeminiMdFilename());
|
||||
@@ -366,17 +367,19 @@ export async function loadCliConfig(
|
||||
|
||||
const fileFiltering = {
|
||||
...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
|
||||
...settings.fileFiltering,
|
||||
...settings.context?.fileFiltering,
|
||||
};
|
||||
|
||||
const includeDirectories = (settings.includeDirectories || [])
|
||||
const includeDirectories = (settings.context?.includeDirectories || [])
|
||||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
|
||||
cwd,
|
||||
settings.loadMemoryFromIncludeDirectories ? includeDirectories : [],
|
||||
settings.context?.loadMemoryFromIncludeDirectories
|
||||
? includeDirectories
|
||||
: [],
|
||||
debugMode,
|
||||
fileService,
|
||||
settings,
|
||||
@@ -452,16 +455,16 @@ export async function loadCliConfig(
|
||||
const blockedMcpServers: Array<{ name: string; extensionName: string }> = [];
|
||||
|
||||
if (!argv.allowedMcpServerNames) {
|
||||
if (settings.allowMCPServers) {
|
||||
if (settings.mcp?.allowed) {
|
||||
mcpServers = allowedMcpServers(
|
||||
mcpServers,
|
||||
settings.allowMCPServers,
|
||||
settings.mcp.allowed,
|
||||
blockedMcpServers,
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.excludeMCPServers) {
|
||||
const excludedNames = new Set(settings.excludeMCPServers.filter(Boolean));
|
||||
if (settings.mcp?.excluded) {
|
||||
const excludedNames = new Set(settings.mcp.excluded.filter(Boolean));
|
||||
if (excludedNames.size > 0) {
|
||||
mcpServers = Object.fromEntries(
|
||||
Object.entries(mcpServers).filter(([key]) => !excludedNames.has(key)),
|
||||
@@ -482,7 +485,7 @@ export async function loadCliConfig(
|
||||
|
||||
// The screen reader argument takes precedence over the accessibility setting.
|
||||
const screenReader =
|
||||
argv.screenReader ?? settings.accessibility?.screenReader ?? false;
|
||||
argv.screenReader ?? settings.ui?.accessibility?.screenReader ?? false;
|
||||
return new Config({
|
||||
sessionId,
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
@@ -490,23 +493,24 @@ export async function loadCliConfig(
|
||||
targetDir: cwd,
|
||||
includeDirectories,
|
||||
loadMemoryFromIncludeDirectories:
|
||||
settings.loadMemoryFromIncludeDirectories || false,
|
||||
settings.context?.loadMemoryFromIncludeDirectories || false,
|
||||
debugMode,
|
||||
question,
|
||||
fullContext: argv.allFiles || false,
|
||||
coreTools: settings.coreTools || undefined,
|
||||
allowedTools: argv.allowedTools || settings.allowedTools || undefined,
|
||||
coreTools: settings.tools?.core || undefined,
|
||||
allowedTools: argv.allowedTools || settings.tools?.allowed || undefined,
|
||||
excludeTools,
|
||||
toolDiscoveryCommand: settings.toolDiscoveryCommand,
|
||||
toolCallCommand: settings.toolCallCommand,
|
||||
mcpServerCommand: settings.mcpServerCommand,
|
||||
toolDiscoveryCommand: settings.tools?.discoveryCommand,
|
||||
toolCallCommand: settings.tools?.callCommand,
|
||||
mcpServerCommand: settings.mcp?.serverCommand,
|
||||
mcpServers,
|
||||
userMemory: memoryContent,
|
||||
geminiMdFileCount: fileCount,
|
||||
approvalMode,
|
||||
showMemoryUsage: argv.showMemoryUsage || settings.showMemoryUsage || false,
|
||||
showMemoryUsage:
|
||||
argv.showMemoryUsage || settings.ui?.showMemoryUsage || false,
|
||||
accessibility: {
|
||||
...settings.accessibility,
|
||||
...settings.ui?.accessibility,
|
||||
screenReader,
|
||||
},
|
||||
telemetry: {
|
||||
@@ -525,16 +529,17 @@ export async function loadCliConfig(
|
||||
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
|
||||
outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile,
|
||||
},
|
||||
usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true,
|
||||
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true,
|
||||
// Git-aware file filtering settings
|
||||
fileFiltering: {
|
||||
respectGitIgnore: settings.fileFiltering?.respectGitIgnore,
|
||||
respectGeminiIgnore: settings.fileFiltering?.respectGeminiIgnore,
|
||||
respectGitIgnore: settings.context?.fileFiltering?.respectGitIgnore,
|
||||
respectGeminiIgnore: settings.context?.fileFiltering?.respectGeminiIgnore,
|
||||
enableRecursiveFileSearch:
|
||||
settings.fileFiltering?.enableRecursiveFileSearch,
|
||||
disableFuzzySearch: settings.fileFiltering?.disableFuzzySearch,
|
||||
settings.context?.fileFiltering?.enableRecursiveFileSearch,
|
||||
disableFuzzySearch: settings.context?.fileFiltering?.disableFuzzySearch,
|
||||
},
|
||||
checkpointing: argv.checkpointing || settings.checkpointing?.enabled,
|
||||
checkpointing:
|
||||
argv.checkpointing || settings.general?.checkpointing?.enabled,
|
||||
proxy:
|
||||
argv.proxy ||
|
||||
process.env['HTTPS_PROXY'] ||
|
||||
@@ -543,26 +548,26 @@ export async function loadCliConfig(
|
||||
process.env['http_proxy'],
|
||||
cwd,
|
||||
fileDiscoveryService: fileService,
|
||||
bugCommand: settings.bugCommand,
|
||||
model: argv.model || settings.model || DEFAULT_GEMINI_MODEL,
|
||||
bugCommand: settings.advanced?.bugCommand,
|
||||
model: argv.model || settings.model?.name || DEFAULT_GEMINI_MODEL,
|
||||
extensionContextFilePaths,
|
||||
maxSessionTurns: settings.maxSessionTurns ?? -1,
|
||||
maxSessionTurns: settings.model?.maxSessionTurns ?? -1,
|
||||
experimentalZedIntegration: argv.experimentalAcp || false,
|
||||
listExtensions: argv.listExtensions || false,
|
||||
extensions: allExtensions,
|
||||
blockedMcpServers,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.summarizeToolOutput,
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
ideMode,
|
||||
chatCompression: settings.chatCompression,
|
||||
chatCompression: settings.model?.chatCompression,
|
||||
folderTrustFeature,
|
||||
folderTrust,
|
||||
interactive,
|
||||
trustedFolder,
|
||||
useRipgrep: settings.useRipgrep,
|
||||
shouldUseNodePtyShell: settings.shouldUseNodePtyShell,
|
||||
skipNextSpeakerCheck: settings.skipNextSpeakerCheck,
|
||||
enablePromptCompletion: settings.enablePromptCompletion ?? false,
|
||||
useRipgrep: settings.tools?.useRipgrep,
|
||||
shouldUseNodePtyShell: settings.tools?.usePty,
|
||||
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
|
||||
enablePromptCompletion: settings.general?.enablePromptCompletion ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -624,7 +629,7 @@ function mergeExcludeTools(
|
||||
extraExcludes?: string[] | undefined,
|
||||
): string[] {
|
||||
const allExcludeTools = new Set([
|
||||
...(settings.excludeTools || []),
|
||||
...(settings.tools?.exclude || []),
|
||||
...(extraExcludes || []),
|
||||
]);
|
||||
for (const extension of extensions) {
|
||||
|
||||
@@ -112,7 +112,7 @@ export function loadExtensions(workspaceDir: string): Extension[] {
|
||||
const disabledExtensions = settings.extensions?.disabled ?? [];
|
||||
const allExtensions = [...loadUserExtensions()];
|
||||
|
||||
if (!settings.extensionManagement) {
|
||||
if (!settings.experimental?.extensionManagement) {
|
||||
allExtensions.push(...getWorkspaceExtensions(workspaceDir));
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function loadSandboxConfig(
|
||||
settings: Settings,
|
||||
argv: SandboxCliArgs,
|
||||
): Promise<SandboxConfig | undefined> {
|
||||
const sandboxOption = argv.sandbox ?? settings.sandbox;
|
||||
const sandboxOption = argv.sandbox ?? settings.tools?.sandbox;
|
||||
const command = getSandboxCommand(sandboxOption);
|
||||
|
||||
const packageJson = await getPackageJson();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ import { DefaultLight } from '../ui/themes/default-light.js';
|
||||
import { DefaultDark } from '../ui/themes/default.js';
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import type { Settings, MemoryImportFormat } from './settingsSchema.js';
|
||||
import { mergeWith } from 'lodash-es';
|
||||
|
||||
export type { Settings, MemoryImportFormat };
|
||||
|
||||
@@ -27,6 +28,58 @@ export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
|
||||
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
|
||||
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
|
||||
|
||||
const MIGRATE_V2_OVERWRITE = false;
|
||||
|
||||
// As defined in spec.md
|
||||
const MIGRATION_MAP: Record<string, string> = {
|
||||
preferredEditor: 'general.preferredEditor',
|
||||
vimMode: 'general.vimMode',
|
||||
disableAutoUpdate: 'general.disableAutoUpdate',
|
||||
disableUpdateNag: 'general.disableUpdateNag',
|
||||
checkpointing: 'general.checkpointing',
|
||||
theme: 'ui.theme',
|
||||
customThemes: 'ui.customThemes',
|
||||
hideWindowTitle: 'ui.hideWindowTitle',
|
||||
hideTips: 'ui.hideTips',
|
||||
hideBanner: 'ui.hideBanner',
|
||||
hideFooter: 'ui.hideFooter',
|
||||
showMemoryUsage: 'ui.showMemoryUsage',
|
||||
showLineNumbers: 'ui.showLineNumbers',
|
||||
accessibility: 'ui.accessibility',
|
||||
ideMode: 'ide.enabled',
|
||||
hasSeenIdeIntegrationNudge: 'ide.hasSeenNudge',
|
||||
usageStatisticsEnabled: 'privacy.usageStatisticsEnabled',
|
||||
telemetry: 'telemetry',
|
||||
model: 'model.name',
|
||||
maxSessionTurns: 'model.maxSessionTurns',
|
||||
summarizeToolOutput: 'model.summarizeToolOutput',
|
||||
chatCompression: 'model.chatCompression',
|
||||
skipNextSpeakerCheck: 'model.skipNextSpeakerCheck',
|
||||
contextFileName: 'context.fileName',
|
||||
memoryImportFormat: 'context.importFormat',
|
||||
memoryDiscoveryMaxDirs: 'context.discoveryMaxDirs',
|
||||
includeDirectories: 'context.includeDirectories',
|
||||
loadMemoryFromIncludeDirectories: 'context.loadFromIncludeDirectories',
|
||||
fileFiltering: 'context.fileFiltering',
|
||||
sandbox: 'tools.sandbox',
|
||||
shouldUseNodePtyShell: 'tools.usePty',
|
||||
coreTools: 'tools.core',
|
||||
excludeTools: 'tools.exclude',
|
||||
toolDiscoveryCommand: 'tools.discoveryCommand',
|
||||
toolCallCommand: 'tools.callCommand',
|
||||
mcpServerCommand: 'mcp.serverCommand',
|
||||
allowMCPServers: 'mcp.allowed',
|
||||
excludeMCPServers: 'mcp.excluded',
|
||||
folderTrustFeature: 'security.folderTrust.featureEnabled',
|
||||
folderTrust: 'security.folderTrust.enabled',
|
||||
selectedAuthType: 'security.auth.selectedType',
|
||||
useExternalAuth: 'security.auth.useExternal',
|
||||
autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
|
||||
dnsResolutionOrder: 'advanced.dnsResolutionOrder',
|
||||
excludedProjectEnvVars: 'advanced.excludedEnvVars',
|
||||
bugCommand: 'advanced.bugCommand',
|
||||
};
|
||||
|
||||
export function getSystemSettingsPath(): string {
|
||||
if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) {
|
||||
return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
|
||||
@@ -82,6 +135,134 @@ export interface SettingsFile {
|
||||
path: string;
|
||||
}
|
||||
|
||||
function setNestedProperty(
|
||||
obj: Record<string, unknown>,
|
||||
path: string,
|
||||
value: unknown,
|
||||
) {
|
||||
const keys = path.split('.');
|
||||
const lastKey = keys.pop();
|
||||
if (!lastKey) return;
|
||||
|
||||
let current: Record<string, unknown> = obj;
|
||||
for (const key of keys) {
|
||||
if (current[key] === undefined) {
|
||||
current[key] = {};
|
||||
}
|
||||
const next = current[key];
|
||||
if (typeof next === 'object' && next !== null) {
|
||||
current = next as Record<string, unknown>;
|
||||
} else {
|
||||
// This path is invalid, so we stop.
|
||||
return;
|
||||
}
|
||||
}
|
||||
current[lastKey] = value;
|
||||
}
|
||||
|
||||
function needsMigration(settings: Record<string, unknown>): boolean {
|
||||
return !('general' in settings);
|
||||
}
|
||||
|
||||
function migrateSettingsToV2(
|
||||
flatSettings: Record<string, unknown>,
|
||||
): Record<string, unknown> | null {
|
||||
if (!needsMigration(flatSettings)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const v2Settings: Record<string, unknown> = {};
|
||||
const flatKeys = new Set(Object.keys(flatSettings));
|
||||
|
||||
for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) {
|
||||
if (flatKeys.has(oldKey)) {
|
||||
setNestedProperty(v2Settings, newPath, flatSettings[oldKey]);
|
||||
flatKeys.delete(oldKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve mcpServers at the top level
|
||||
if (flatSettings['mcpServers']) {
|
||||
v2Settings['mcpServers'] = flatSettings['mcpServers'];
|
||||
flatKeys.delete('mcpServers');
|
||||
}
|
||||
|
||||
// Carry over any unrecognized keys
|
||||
for (const remainingKey of flatKeys) {
|
||||
v2Settings[remainingKey] = flatSettings[remainingKey];
|
||||
}
|
||||
|
||||
return v2Settings;
|
||||
}
|
||||
|
||||
function getNestedProperty(
|
||||
obj: Record<string, unknown>,
|
||||
path: string,
|
||||
): unknown {
|
||||
const keys = path.split('.');
|
||||
let current: unknown = obj;
|
||||
for (const key of keys) {
|
||||
if (typeof current !== 'object' || current === null || !(key in current)) {
|
||||
return undefined;
|
||||
}
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
const REVERSE_MIGRATION_MAP: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(MIGRATION_MAP).map(([key, value]) => [value, key]),
|
||||
);
|
||||
|
||||
// Dynamically determine the top-level keys from the V2 settings structure.
|
||||
const KNOWN_V2_CONTAINERS = new Set(
|
||||
Object.values(MIGRATION_MAP).map((path) => path.split('.')[0]),
|
||||
);
|
||||
|
||||
export function migrateSettingsToV1(
|
||||
v2Settings: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const v1Settings: Record<string, unknown> = {};
|
||||
const v2Keys = new Set(Object.keys(v2Settings));
|
||||
|
||||
for (const [newPath, oldKey] of Object.entries(REVERSE_MIGRATION_MAP)) {
|
||||
const value = getNestedProperty(v2Settings, newPath);
|
||||
if (value !== undefined) {
|
||||
v1Settings[oldKey] = value;
|
||||
v2Keys.delete(newPath.split('.')[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve mcpServers at the top level
|
||||
if (v2Settings['mcpServers']) {
|
||||
v1Settings['mcpServers'] = v2Settings['mcpServers'];
|
||||
v2Keys.delete('mcpServers');
|
||||
}
|
||||
|
||||
// Carry over any unrecognized keys
|
||||
for (const remainingKey of v2Keys) {
|
||||
const value = v2Settings[remainingKey];
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't carry over empty objects that were just containers for migrated settings.
|
||||
if (
|
||||
KNOWN_V2_CONTAINERS.has(remainingKey) &&
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Object.keys(value).length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
v1Settings[remainingKey] = value;
|
||||
}
|
||||
|
||||
return v1Settings;
|
||||
}
|
||||
|
||||
function mergeSettings(
|
||||
system: Settings,
|
||||
systemDefaults: Settings,
|
||||
@@ -92,8 +273,17 @@ function mergeSettings(
|
||||
const safeWorkspace = isTrusted ? workspace : ({} as Settings);
|
||||
|
||||
// folderTrust is not supported at workspace level.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { folderTrust, ...safeWorkspaceWithoutFolderTrust } = safeWorkspace;
|
||||
const { security, ...restOfWorkspace } = safeWorkspace;
|
||||
const safeWorkspaceWithoutFolderTrust = security
|
||||
? {
|
||||
...restOfWorkspace,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
security: (({ folderTrust, ...rest }) => rest)(security),
|
||||
}
|
||||
: {
|
||||
...restOfWorkspace,
|
||||
security: {},
|
||||
};
|
||||
|
||||
// Settings are merged with the following precedence (last one wins for
|
||||
// single values):
|
||||
@@ -109,40 +299,84 @@ function mergeSettings(
|
||||
...user,
|
||||
...safeWorkspaceWithoutFolderTrust,
|
||||
...system,
|
||||
customThemes: {
|
||||
...(systemDefaults.customThemes || {}),
|
||||
...(user.customThemes || {}),
|
||||
...(safeWorkspace.customThemes || {}),
|
||||
...(system.customThemes || {}),
|
||||
ui: {
|
||||
...(systemDefaults.ui || {}),
|
||||
...(user.ui || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.ui || {}),
|
||||
...(system.ui || {}),
|
||||
customThemes: {
|
||||
...(systemDefaults.ui?.customThemes || {}),
|
||||
...(user.ui?.customThemes || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.ui?.customThemes || {}),
|
||||
...(system.ui?.customThemes || {}),
|
||||
},
|
||||
},
|
||||
security: {
|
||||
...(systemDefaults.security || {}),
|
||||
...(user.security || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.security || {}),
|
||||
...(system.security || {}),
|
||||
},
|
||||
mcp: {
|
||||
...(systemDefaults.mcp || {}),
|
||||
...(user.mcp || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.mcp || {}),
|
||||
...(system.mcp || {}),
|
||||
},
|
||||
mcpServers: {
|
||||
...(systemDefaults.mcpServers || {}),
|
||||
...(user.mcpServers || {}),
|
||||
...(safeWorkspace.mcpServers || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.mcpServers || {}),
|
||||
...(system.mcpServers || {}),
|
||||
},
|
||||
includeDirectories: [
|
||||
...(systemDefaults.includeDirectories || []),
|
||||
...(user.includeDirectories || []),
|
||||
...(safeWorkspace.includeDirectories || []),
|
||||
...(system.includeDirectories || []),
|
||||
],
|
||||
chatCompression: {
|
||||
...(systemDefaults.chatCompression || {}),
|
||||
...(user.chatCompression || {}),
|
||||
...(safeWorkspace.chatCompression || {}),
|
||||
...(system.chatCompression || {}),
|
||||
context: {
|
||||
...(systemDefaults.context || {}),
|
||||
...(user.context || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.context || {}),
|
||||
...(system.context || {}),
|
||||
includeDirectories: [
|
||||
...(systemDefaults.context?.includeDirectories || []),
|
||||
...(user.context?.includeDirectories || []),
|
||||
...(safeWorkspaceWithoutFolderTrust.context?.includeDirectories || []),
|
||||
...(system.context?.includeDirectories || []),
|
||||
],
|
||||
},
|
||||
model: {
|
||||
...(systemDefaults.model || {}),
|
||||
...(user.model || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.model || {}),
|
||||
...(system.model || {}),
|
||||
chatCompression: {
|
||||
...(systemDefaults.model?.chatCompression || {}),
|
||||
...(user.model?.chatCompression || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.model?.chatCompression || {}),
|
||||
...(system.model?.chatCompression || {}),
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
...(systemDefaults.advanced || {}),
|
||||
...(user.advanced || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.advanced || {}),
|
||||
...(system.advanced || {}),
|
||||
excludedEnvVars: [
|
||||
...new Set([
|
||||
...(systemDefaults.advanced?.excludedEnvVars || []),
|
||||
...(user.advanced?.excludedEnvVars || []),
|
||||
...(safeWorkspaceWithoutFolderTrust.advanced?.excludedEnvVars || []),
|
||||
...(system.advanced?.excludedEnvVars || []),
|
||||
]),
|
||||
],
|
||||
},
|
||||
extensions: {
|
||||
...(systemDefaults.extensions || {}),
|
||||
...(user.extensions || {}),
|
||||
...(safeWorkspace.extensions || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.extensions || {}),
|
||||
...(system.extensions || {}),
|
||||
disabled: [
|
||||
...new Set([
|
||||
...(systemDefaults.extensions?.disabled || []),
|
||||
...(user.extensions?.disabled || []),
|
||||
...(safeWorkspace.extensions?.disabled || []),
|
||||
...(safeWorkspaceWithoutFolderTrust.extensions?.disabled || []),
|
||||
...(system.extensions?.disabled || []),
|
||||
]),
|
||||
],
|
||||
@@ -150,7 +384,8 @@ function mergeSettings(
|
||||
...new Set([
|
||||
...(systemDefaults.extensions?.workspacesWithMigrationNudge || []),
|
||||
...(user.extensions?.workspacesWithMigrationNudge || []),
|
||||
...(safeWorkspace.extensions?.workspacesWithMigrationNudge || []),
|
||||
...(safeWorkspaceWithoutFolderTrust.extensions
|
||||
?.workspacesWithMigrationNudge || []),
|
||||
...(system.extensions?.workspacesWithMigrationNudge || []),
|
||||
]),
|
||||
],
|
||||
@@ -166,6 +401,7 @@ export class LoadedSettings {
|
||||
workspace: SettingsFile,
|
||||
errors: SettingsError[],
|
||||
isTrusted: boolean,
|
||||
migratedInMemorScopes: Set<SettingScope>,
|
||||
) {
|
||||
this.system = system;
|
||||
this.systemDefaults = systemDefaults;
|
||||
@@ -173,6 +409,7 @@ export class LoadedSettings {
|
||||
this.workspace = workspace;
|
||||
this.errors = errors;
|
||||
this.isTrusted = isTrusted;
|
||||
this.migratedInMemorScopes = migratedInMemorScopes;
|
||||
this._merged = this.computeMergedSettings();
|
||||
}
|
||||
|
||||
@@ -182,6 +419,7 @@ export class LoadedSettings {
|
||||
readonly workspace: SettingsFile;
|
||||
readonly errors: SettingsError[];
|
||||
readonly isTrusted: boolean;
|
||||
readonly migratedInMemorScopes: Set<SettingScope>;
|
||||
|
||||
private _merged: Settings;
|
||||
|
||||
@@ -214,13 +452,9 @@ export class LoadedSettings {
|
||||
}
|
||||
}
|
||||
|
||||
setValue<K extends keyof Settings>(
|
||||
scope: SettingScope,
|
||||
key: K,
|
||||
value: Settings[K],
|
||||
): void {
|
||||
setValue(scope: SettingScope, key: string, value: unknown): void {
|
||||
const settingsFile = this.forScope(scope);
|
||||
settingsFile.settings[key] = value;
|
||||
setNestedProperty(settingsFile.settings, key, value);
|
||||
this._merged = this.computeMergedSettings();
|
||||
saveSettings(settingsFile);
|
||||
}
|
||||
@@ -357,7 +591,8 @@ export function loadEnvironment(settings?: Settings): void {
|
||||
const parsedEnv = dotenv.parse(envFileContent);
|
||||
|
||||
const excludedVars =
|
||||
resolvedSettings?.excludedProjectEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
|
||||
resolvedSettings?.advanced?.excludedEnvVars ||
|
||||
DEFAULT_EXCLUDED_ENV_VARS;
|
||||
const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR);
|
||||
|
||||
for (const key in parsedEnv) {
|
||||
@@ -391,6 +626,7 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
|
||||
const settingsErrors: SettingsError[] = [];
|
||||
const systemSettingsPath = getSystemSettingsPath();
|
||||
const systemDefaultsPath = getSystemDefaultsPath();
|
||||
const migratedInMemorScopes = new Set<SettingScope>();
|
||||
|
||||
// Resolve paths to their canonical representation to handle symlinks
|
||||
const resolvedWorkspaceDir = path.resolve(workspaceDir);
|
||||
@@ -411,85 +647,97 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
|
||||
workspaceDir,
|
||||
).getWorkspaceSettingsPath();
|
||||
|
||||
// Load system settings
|
||||
try {
|
||||
if (fs.existsSync(systemSettingsPath)) {
|
||||
const systemContent = fs.readFileSync(systemSettingsPath, 'utf-8');
|
||||
systemSettings = JSON.parse(stripJsonComments(systemContent)) as Settings;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
settingsErrors.push({
|
||||
message: getErrorMessage(error),
|
||||
path: systemSettingsPath,
|
||||
});
|
||||
}
|
||||
|
||||
// Load system defaults
|
||||
try {
|
||||
if (fs.existsSync(systemDefaultsPath)) {
|
||||
const systemDefaultsContent = fs.readFileSync(
|
||||
systemDefaultsPath,
|
||||
'utf-8',
|
||||
);
|
||||
const parsedSystemDefaults = JSON.parse(
|
||||
stripJsonComments(systemDefaultsContent),
|
||||
) as Settings;
|
||||
systemDefaultSettings = resolveEnvVarsInObject(parsedSystemDefaults);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
settingsErrors.push({
|
||||
message: getErrorMessage(error),
|
||||
path: systemDefaultsPath,
|
||||
});
|
||||
}
|
||||
|
||||
// Load user settings
|
||||
try {
|
||||
if (fs.existsSync(USER_SETTINGS_PATH)) {
|
||||
const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
|
||||
userSettings = JSON.parse(stripJsonComments(userContent)) as Settings;
|
||||
// Support legacy theme names
|
||||
if (userSettings.theme && userSettings.theme === 'VS') {
|
||||
userSettings.theme = DefaultLight.name;
|
||||
} else if (userSettings.theme && userSettings.theme === 'VS2015') {
|
||||
userSettings.theme = DefaultDark.name;
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
settingsErrors.push({
|
||||
message: getErrorMessage(error),
|
||||
path: USER_SETTINGS_PATH,
|
||||
});
|
||||
}
|
||||
|
||||
if (realWorkspaceDir !== realHomeDir) {
|
||||
// Load workspace settings
|
||||
const loadAndMigrate = (filePath: string, scope: SettingScope): Settings => {
|
||||
try {
|
||||
if (fs.existsSync(workspaceSettingsPath)) {
|
||||
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
|
||||
workspaceSettings = JSON.parse(
|
||||
stripJsonComments(projectContent),
|
||||
) as Settings;
|
||||
if (workspaceSettings.theme && workspaceSettings.theme === 'VS') {
|
||||
workspaceSettings.theme = DefaultLight.name;
|
||||
} else if (
|
||||
workspaceSettings.theme &&
|
||||
workspaceSettings.theme === 'VS2015'
|
||||
if (fs.existsSync(filePath)) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const rawSettings: unknown = JSON.parse(stripJsonComments(content));
|
||||
|
||||
if (
|
||||
typeof rawSettings !== 'object' ||
|
||||
rawSettings === null ||
|
||||
Array.isArray(rawSettings)
|
||||
) {
|
||||
workspaceSettings.theme = DefaultDark.name;
|
||||
settingsErrors.push({
|
||||
message: 'Settings file is not a valid JSON object.',
|
||||
path: filePath,
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
let settingsObject = rawSettings as Record<string, unknown>;
|
||||
if (needsMigration(settingsObject)) {
|
||||
console.error(`Legacy settings file detected at: ${filePath}`);
|
||||
const migratedSettings = migrateSettingsToV2(settingsObject);
|
||||
if (migratedSettings) {
|
||||
if (MIGRATE_V2_OVERWRITE) {
|
||||
try {
|
||||
fs.renameSync(filePath, `${filePath}.orig`);
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
JSON.stringify(migratedSettings, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
console.log(
|
||||
`Successfully migrated and saved settings file: ${filePath}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Error migrating settings file on disk: ${getErrorMessage(
|
||||
e,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Successfully migrated settings for ${filePath} in-memory for the current session.`,
|
||||
);
|
||||
migratedInMemorScopes.add(scope);
|
||||
}
|
||||
settingsObject = migratedSettings;
|
||||
}
|
||||
}
|
||||
return settingsObject as Settings;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
settingsErrors.push({
|
||||
message: getErrorMessage(error),
|
||||
path: workspaceSettingsPath,
|
||||
path: filePath,
|
||||
});
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
systemSettings = loadAndMigrate(systemSettingsPath, SettingScope.System);
|
||||
systemDefaultSettings = loadAndMigrate(
|
||||
systemDefaultsPath,
|
||||
SettingScope.SystemDefaults,
|
||||
);
|
||||
userSettings = loadAndMigrate(USER_SETTINGS_PATH, SettingScope.User);
|
||||
|
||||
if (realWorkspaceDir !== realHomeDir) {
|
||||
workspaceSettings = loadAndMigrate(
|
||||
workspaceSettingsPath,
|
||||
SettingScope.Workspace,
|
||||
);
|
||||
}
|
||||
|
||||
// Support legacy theme names
|
||||
if (userSettings.ui?.theme === 'VS') {
|
||||
userSettings.ui.theme = DefaultLight.name;
|
||||
} else if (userSettings.ui?.theme === 'VS2015') {
|
||||
userSettings.ui.theme = DefaultDark.name;
|
||||
}
|
||||
if (workspaceSettings.ui?.theme === 'VS') {
|
||||
workspaceSettings.ui.theme = DefaultLight.name;
|
||||
} else if (workspaceSettings.ui?.theme === 'VS2015') {
|
||||
workspaceSettings.ui.theme = DefaultDark.name;
|
||||
}
|
||||
|
||||
// For the initial trust check, we can only use user and system settings.
|
||||
const initialTrustCheckSettings = { ...systemSettings, ...userSettings };
|
||||
const isTrusted = isWorkspaceTrusted(initialTrustCheckSettings) ?? true;
|
||||
const initialTrustCheckSettings = mergeWith({}, systemSettings, userSettings);
|
||||
const isTrusted =
|
||||
isWorkspaceTrusted(initialTrustCheckSettings as Settings) ?? true;
|
||||
|
||||
// Create a temporary merged settings object to pass to loadEnvironment.
|
||||
const tempMergedSettings = mergeSettings(
|
||||
@@ -529,21 +777,9 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
|
||||
},
|
||||
settingsErrors,
|
||||
isTrusted,
|
||||
migratedInMemorScopes,
|
||||
);
|
||||
|
||||
// Validate chatCompression settings
|
||||
const chatCompression = loadedSettings.merged.chatCompression;
|
||||
const threshold = chatCompression?.contextPercentageThreshold;
|
||||
if (
|
||||
threshold != null &&
|
||||
(typeof threshold !== 'number' || threshold < 0 || threshold > 1)
|
||||
) {
|
||||
console.warn(
|
||||
`Invalid value for chatCompression.contextPercentageThreshold: "${threshold}". Please use a value between 0 and 1. Using default compression settings.`,
|
||||
);
|
||||
delete loadedSettings.merged.chatCompression;
|
||||
}
|
||||
|
||||
return loadedSettings;
|
||||
}
|
||||
|
||||
@@ -555,9 +791,16 @@ export function saveSettings(settingsFile: SettingsFile): void {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
let settingsToSave = settingsFile.settings;
|
||||
if (!MIGRATE_V2_OVERWRITE) {
|
||||
settingsToSave = migrateSettingsToV1(
|
||||
settingsToSave as Record<string, unknown>,
|
||||
) as Settings;
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
settingsFile.path,
|
||||
JSON.stringify(settingsFile.settings, null, 2),
|
||||
JSON.stringify(settingsToSave, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,49 +12,18 @@ describe('SettingsSchema', () => {
|
||||
describe('SETTINGS_SCHEMA', () => {
|
||||
it('should contain all expected top-level settings', () => {
|
||||
const expectedSettings = [
|
||||
'theme',
|
||||
'customThemes',
|
||||
'showMemoryUsage',
|
||||
'usageStatisticsEnabled',
|
||||
'autoConfigureMaxOldSpaceSize',
|
||||
'preferredEditor',
|
||||
'maxSessionTurns',
|
||||
'memoryImportFormat',
|
||||
'memoryDiscoveryMaxDirs',
|
||||
'contextFileName',
|
||||
'vimMode',
|
||||
'ideMode',
|
||||
'accessibility',
|
||||
'checkpointing',
|
||||
'fileFiltering',
|
||||
'disableAutoUpdate',
|
||||
'hideWindowTitle',
|
||||
'hideTips',
|
||||
'hideBanner',
|
||||
'selectedAuthType',
|
||||
'useExternalAuth',
|
||||
'sandbox',
|
||||
'coreTools',
|
||||
'excludeTools',
|
||||
'toolDiscoveryCommand',
|
||||
'toolCallCommand',
|
||||
'mcpServerCommand',
|
||||
'mcpServers',
|
||||
'allowMCPServers',
|
||||
'excludeMCPServers',
|
||||
'general',
|
||||
'ui',
|
||||
'ide',
|
||||
'privacy',
|
||||
'telemetry',
|
||||
'bugCommand',
|
||||
'summarizeToolOutput',
|
||||
'dnsResolutionOrder',
|
||||
'excludedProjectEnvVars',
|
||||
'disableUpdateNag',
|
||||
'includeDirectories',
|
||||
'loadMemoryFromIncludeDirectories',
|
||||
'model',
|
||||
'hasSeenIdeIntegrationNudge',
|
||||
'folderTrustFeature',
|
||||
'useRipgrep',
|
||||
'debugKeystrokeLogging',
|
||||
'context',
|
||||
'tools',
|
||||
'mcp',
|
||||
'security',
|
||||
'advanced',
|
||||
];
|
||||
|
||||
expectedSettings.forEach((setting) => {
|
||||
@@ -80,9 +49,16 @@ describe('SettingsSchema', () => {
|
||||
|
||||
it('should have correct nested setting structure', () => {
|
||||
const nestedSettings = [
|
||||
'accessibility',
|
||||
'checkpointing',
|
||||
'fileFiltering',
|
||||
'general',
|
||||
'ui',
|
||||
'ide',
|
||||
'privacy',
|
||||
'model',
|
||||
'context',
|
||||
'tools',
|
||||
'mcp',
|
||||
'security',
|
||||
'advanced',
|
||||
];
|
||||
|
||||
nestedSettings.forEach((setting) => {
|
||||
@@ -99,29 +75,36 @@ describe('SettingsSchema', () => {
|
||||
|
||||
it('should have accessibility nested properties', () => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA.accessibility.properties?.disableLoadingPhrases,
|
||||
SETTINGS_SCHEMA.ui?.properties?.accessibility?.properties,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.accessibility.properties?.disableLoadingPhrases.type,
|
||||
SETTINGS_SCHEMA.ui?.properties?.accessibility.properties
|
||||
?.disableLoadingPhrases.type,
|
||||
).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should have checkpointing nested properties', () => {
|
||||
expect(SETTINGS_SCHEMA.checkpointing.properties?.enabled).toBeDefined();
|
||||
expect(SETTINGS_SCHEMA.checkpointing.properties?.enabled.type).toBe(
|
||||
'boolean',
|
||||
);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled
|
||||
.type,
|
||||
).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should have fileFiltering nested properties', () => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA.fileFiltering.properties?.respectGitIgnore,
|
||||
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
|
||||
?.respectGitIgnore,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.fileFiltering.properties?.respectGeminiIgnore,
|
||||
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
|
||||
?.respectGeminiIgnore,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.fileFiltering.properties?.enableRecursiveFileSearch,
|
||||
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
|
||||
?.enableRecursiveFileSearch,
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -150,11 +133,6 @@ describe('SettingsSchema', () => {
|
||||
expect(categories.size).toBeGreaterThan(0);
|
||||
expect(categories).toContain('General');
|
||||
expect(categories).toContain('UI');
|
||||
expect(categories).toContain('Mode');
|
||||
expect(categories).toContain('Updates');
|
||||
expect(categories).toContain('Accessibility');
|
||||
expect(categories).toContain('Checkpointing');
|
||||
expect(categories).toContain('File Filtering');
|
||||
expect(categories).toContain('Advanced');
|
||||
});
|
||||
|
||||
@@ -183,85 +161,148 @@ describe('SettingsSchema', () => {
|
||||
|
||||
it('should have showInDialog property configured', () => {
|
||||
// Check that user-facing settings are marked for dialog display
|
||||
expect(SETTINGS_SCHEMA.showMemoryUsage.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.vimMode.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.ideMode.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.disableAutoUpdate.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.hideWindowTitle.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.hideTips.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.hideBanner.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.usageStatisticsEnabled.showInDialog).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.ui.properties.showMemoryUsage.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(SETTINGS_SCHEMA.general.properties.vimMode.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(SETTINGS_SCHEMA.ide.properties.enabled.showInDialog).toBe(true);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.disableAutoUpdate.showInDialog,
|
||||
).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.ui.properties.hideWindowTitle.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(SETTINGS_SCHEMA.ui.properties.hideTips.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.ui.properties.hideBanner.showInDialog).toBe(true);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.privacy.properties.usageStatisticsEnabled.showInDialog,
|
||||
).toBe(false);
|
||||
|
||||
// Check that advanced settings are hidden from dialog
|
||||
expect(SETTINGS_SCHEMA.selectedAuthType.showInDialog).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.coreTools.showInDialog).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.security.properties.auth.showInDialog).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.tools.properties.core.showInDialog).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.mcpServers.showInDialog).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.telemetry.showInDialog).toBe(false);
|
||||
|
||||
// Check that some settings are appropriately hidden
|
||||
expect(SETTINGS_SCHEMA.theme.showInDialog).toBe(false); // Changed to false
|
||||
expect(SETTINGS_SCHEMA.customThemes.showInDialog).toBe(false); // Managed via theme editor
|
||||
expect(SETTINGS_SCHEMA.checkpointing.showInDialog).toBe(false); // Experimental feature
|
||||
expect(SETTINGS_SCHEMA.accessibility.showInDialog).toBe(false); // Changed to false
|
||||
expect(SETTINGS_SCHEMA.fileFiltering.showInDialog).toBe(false); // Changed to false
|
||||
expect(SETTINGS_SCHEMA.preferredEditor.showInDialog).toBe(false); // Changed to false
|
||||
expect(SETTINGS_SCHEMA.autoConfigureMaxOldSpaceSize.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(SETTINGS_SCHEMA.ui.properties.theme.showInDialog).toBe(false); // Changed to false
|
||||
expect(SETTINGS_SCHEMA.ui.properties.customThemes.showInDialog).toBe(
|
||||
false,
|
||||
); // Managed via theme editor
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.checkpointing.showInDialog,
|
||||
).toBe(false); // Experimental feature
|
||||
expect(SETTINGS_SCHEMA.ui.properties.accessibility.showInDialog).toBe(
|
||||
false,
|
||||
); // Changed to false
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context.properties.fileFiltering.showInDialog,
|
||||
).toBe(false); // Changed to false
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.preferredEditor.showInDialog,
|
||||
).toBe(false); // Changed to false
|
||||
expect(
|
||||
SETTINGS_SCHEMA.advanced.properties.autoConfigureMemory.showInDialog,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should infer Settings type correctly', () => {
|
||||
// This test ensures that the Settings type is properly inferred from the schema
|
||||
const settings: Settings = {
|
||||
theme: 'dark',
|
||||
includeDirectories: ['/path/to/dir'],
|
||||
loadMemoryFromIncludeDirectories: true,
|
||||
ui: {
|
||||
theme: 'dark',
|
||||
},
|
||||
context: {
|
||||
includeDirectories: ['/path/to/dir'],
|
||||
loadMemoryFromIncludeDirectories: true,
|
||||
},
|
||||
};
|
||||
|
||||
// TypeScript should not complain about these properties
|
||||
expect(settings.theme).toBe('dark');
|
||||
expect(settings.includeDirectories).toEqual(['/path/to/dir']);
|
||||
expect(settings.loadMemoryFromIncludeDirectories).toBe(true);
|
||||
expect(settings.ui?.theme).toBe('dark');
|
||||
expect(settings.context?.includeDirectories).toEqual(['/path/to/dir']);
|
||||
expect(settings.context?.loadMemoryFromIncludeDirectories).toBe(true);
|
||||
});
|
||||
|
||||
it('should have includeDirectories setting in schema', () => {
|
||||
expect(SETTINGS_SCHEMA.includeDirectories).toBeDefined();
|
||||
expect(SETTINGS_SCHEMA.includeDirectories.type).toBe('array');
|
||||
expect(SETTINGS_SCHEMA.includeDirectories.category).toBe('General');
|
||||
expect(SETTINGS_SCHEMA.includeDirectories.default).toEqual([]);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.includeDirectories,
|
||||
).toBeDefined();
|
||||
expect(SETTINGS_SCHEMA.context?.properties.includeDirectories.type).toBe(
|
||||
'array',
|
||||
);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.includeDirectories.category,
|
||||
).toBe('Context');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.includeDirectories.default,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('should have loadMemoryFromIncludeDirectories setting in schema', () => {
|
||||
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories).toBeDefined();
|
||||
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.type).toBe(
|
||||
'boolean',
|
||||
);
|
||||
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.category).toBe(
|
||||
'General',
|
||||
);
|
||||
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.default).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
|
||||
.type,
|
||||
).toBe('boolean');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
|
||||
.category,
|
||||
).toBe('Context');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
|
||||
.default,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should have folderTrustFeature setting in schema', () => {
|
||||
expect(SETTINGS_SCHEMA.folderTrustFeature).toBeDefined();
|
||||
expect(SETTINGS_SCHEMA.folderTrustFeature.type).toBe('boolean');
|
||||
expect(SETTINGS_SCHEMA.folderTrustFeature.category).toBe('General');
|
||||
expect(SETTINGS_SCHEMA.folderTrustFeature.default).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.folderTrustFeature.showInDialog).toBe(true);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled.type,
|
||||
).toBe('boolean');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
|
||||
.category,
|
||||
).toBe('Security');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
|
||||
.default,
|
||||
).toBe(false);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
|
||||
.showInDialog,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should have debugKeystrokeLogging setting in schema', () => {
|
||||
expect(SETTINGS_SCHEMA.debugKeystrokeLogging).toBeDefined();
|
||||
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.type).toBe('boolean');
|
||||
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.category).toBe('General');
|
||||
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.default).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.requiresRestart).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.description).toBe(
|
||||
'Enable debug logging of keystrokes to the console.',
|
||||
);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.type,
|
||||
).toBe('boolean');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.category,
|
||||
).toBe('General');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.default,
|
||||
).toBe(false);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging
|
||||
.requiresRestart,
|
||||
).toBe(false);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.showInDialog,
|
||||
).toBe(true);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.description,
|
||||
).toBe('Enable debug logging of keystrokes to the console.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -132,8 +132,12 @@ describe('isWorkspaceTrusted', () => {
|
||||
let mockCwd: string;
|
||||
const mockRules: Record<string, TrustLevel> = {};
|
||||
const mockSettings: Settings = {
|
||||
folderTrustFeature: true,
|
||||
folderTrust: true,
|
||||
security: {
|
||||
folderTrust: {
|
||||
featureEnabled: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -111,8 +111,9 @@ export function saveTrustedFolders(
|
||||
}
|
||||
|
||||
export function isWorkspaceTrusted(settings: Settings): boolean | undefined {
|
||||
const folderTrustFeature = settings.folderTrustFeature ?? false;
|
||||
const folderTrustSetting = settings.folderTrust ?? true;
|
||||
const folderTrustFeature =
|
||||
settings.security?.folderTrust?.featureEnabled ?? false;
|
||||
const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true;
|
||||
const folderTrustEnabled = folderTrustFeature && folderTrustSetting;
|
||||
|
||||
if (!folderTrustEnabled) {
|
||||
|
||||
Reference in New Issue
Block a user