mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
refactor: refactor settings to a nested structure (#7244)
This commit is contained in:
25
package-lock.json
generated
25
package-lock.json
generated
@@ -3284,6 +3284,23 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/marked": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
|
||||
@@ -9280,6 +9297,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
@@ -14500,6 +14523,7 @@
|
||||
"ink": "^6.1.1",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lowlight": "^3.3.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"open": "^10.1.2",
|
||||
@@ -14524,6 +14548,7 @@
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"ink": "^6.1.1",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lowlight": "^3.3.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"open": "^10.1.2",
|
||||
@@ -63,6 +64,7 @@
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
|
||||
@@ -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 = {
|
||||
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,9 +1674,11 @@ describe('loadCliConfig chatCompression', () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
model: {
|
||||
chatCompression: {
|
||||
contextPercentageThreshold: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
const config = await loadCliConfig(settings, [], 'test-session', argv);
|
||||
expect(config.getChatCompression()).toEqual({
|
||||
@@ -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.
|
||||
const { security, ...restOfWorkspace } = safeWorkspace;
|
||||
const safeWorkspaceWithoutFolderTrust = security
|
||||
? {
|
||||
...restOfWorkspace,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { folderTrust, ...safeWorkspaceWithoutFolderTrust } = safeWorkspace;
|
||||
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,
|
||||
ui: {
|
||||
...(systemDefaults.ui || {}),
|
||||
...(user.ui || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.ui || {}),
|
||||
...(system.ui || {}),
|
||||
customThemes: {
|
||||
...(systemDefaults.customThemes || {}),
|
||||
...(user.customThemes || {}),
|
||||
...(safeWorkspace.customThemes || {}),
|
||||
...(system.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 || {}),
|
||||
},
|
||||
context: {
|
||||
...(systemDefaults.context || {}),
|
||||
...(user.context || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.context || {}),
|
||||
...(system.context || {}),
|
||||
includeDirectories: [
|
||||
...(systemDefaults.includeDirectories || []),
|
||||
...(user.includeDirectories || []),
|
||||
...(safeWorkspace.includeDirectories || []),
|
||||
...(system.includeDirectories || []),
|
||||
...(systemDefaults.context?.includeDirectories || []),
|
||||
...(user.context?.includeDirectories || []),
|
||||
...(safeWorkspaceWithoutFolderTrust.context?.includeDirectories || []),
|
||||
...(system.context?.includeDirectories || []),
|
||||
],
|
||||
},
|
||||
model: {
|
||||
...(systemDefaults.model || {}),
|
||||
...(user.model || {}),
|
||||
...(safeWorkspaceWithoutFolderTrust.model || {}),
|
||||
...(system.model || {}),
|
||||
chatCompression: {
|
||||
...(systemDefaults.chatCompression || {}),
|
||||
...(user.chatCompression || {}),
|
||||
...(safeWorkspace.chatCompression || {}),
|
||||
...(system.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
|
||||
const loadAndMigrate = (filePath: string, scope: SettingScope): Settings => {
|
||||
try {
|
||||
if (fs.existsSync(systemSettingsPath)) {
|
||||
const systemContent = fs.readFileSync(systemSettingsPath, 'utf-8');
|
||||
systemSettings = JSON.parse(stripJsonComments(systemContent)) as Settings;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
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)
|
||||
) {
|
||||
settingsErrors.push({
|
||||
message: getErrorMessage(error),
|
||||
path: systemSettingsPath,
|
||||
message: 'Settings file is not a valid JSON object.',
|
||||
path: filePath,
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
// Load system defaults
|
||||
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 {
|
||||
if (fs.existsSync(systemDefaultsPath)) {
|
||||
const systemDefaultsContent = fs.readFileSync(
|
||||
systemDefaultsPath,
|
||||
fs.renameSync(filePath, `${filePath}.orig`);
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
JSON.stringify(migratedSettings, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
const parsedSystemDefaults = JSON.parse(
|
||||
stripJsonComments(systemDefaultsContent),
|
||||
) as Settings;
|
||||
systemDefaultSettings = resolveEnvVarsInObject(parsedSystemDefaults);
|
||||
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: systemDefaultsPath,
|
||||
path: filePath,
|
||||
});
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
systemSettings = loadAndMigrate(systemSettingsPath, SettingScope.System);
|
||||
systemDefaultSettings = loadAndMigrate(
|
||||
systemDefaultsPath,
|
||||
SettingScope.SystemDefaults,
|
||||
);
|
||||
userSettings = loadAndMigrate(USER_SETTINGS_PATH, SettingScope.User);
|
||||
|
||||
if (realWorkspaceDir !== realHomeDir) {
|
||||
// Load workspace 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'
|
||||
) {
|
||||
workspaceSettings.theme = DefaultDark.name;
|
||||
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;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
settingsErrors.push({
|
||||
message: getErrorMessage(error),
|
||||
path: workspaceSettingsPath,
|
||||
});
|
||||
}
|
||||
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 = {
|
||||
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.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,113 @@ export type DnsResolutionOrder = 'ipv4first' | 'verbatim';
|
||||
* `as const` is crucial for TypeScript to infer the most specific types possible.
|
||||
*/
|
||||
export const SETTINGS_SCHEMA = {
|
||||
// UI Settings
|
||||
// Maintained for compatibility/criticality
|
||||
mcpServers: {
|
||||
type: 'object',
|
||||
label: 'MCP Servers',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: {} as Record<string, MCPServerConfig>,
|
||||
description: 'Configuration for MCP servers.',
|
||||
showInDialog: false,
|
||||
},
|
||||
|
||||
general: {
|
||||
type: 'object',
|
||||
label: 'General',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'General application settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
preferredEditor: {
|
||||
type: 'string',
|
||||
label: 'Preferred Editor',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
description: 'The preferred editor to open files in.',
|
||||
showInDialog: false,
|
||||
},
|
||||
vimMode: {
|
||||
type: 'boolean',
|
||||
label: 'Vim Mode',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Enable Vim keybindings',
|
||||
showInDialog: true,
|
||||
},
|
||||
disableAutoUpdate: {
|
||||
type: 'boolean',
|
||||
label: 'Disable Auto Update',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Disable automatic updates',
|
||||
showInDialog: true,
|
||||
},
|
||||
disableUpdateNag: {
|
||||
type: 'boolean',
|
||||
label: 'Disable Update Nag',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Disable update notification prompts.',
|
||||
showInDialog: false,
|
||||
},
|
||||
checkpointing: {
|
||||
type: 'object',
|
||||
label: 'Checkpointing',
|
||||
category: 'General',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Session checkpointing settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Checkpointing',
|
||||
category: 'General',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable session checkpointing for recovery',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
enablePromptCompletion: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Prompt Completion',
|
||||
category: 'General',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable AI-powered prompt completion suggestions while typing.',
|
||||
showInDialog: true,
|
||||
},
|
||||
debugKeystrokeLogging: {
|
||||
type: 'boolean',
|
||||
label: 'Debug Keystroke Logging',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Enable debug logging of keystrokes to the console.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
ui: {
|
||||
type: 'object',
|
||||
label: 'UI',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'User interface settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
theme: {
|
||||
type: 'string',
|
||||
label: 'Theme',
|
||||
@@ -104,94 +210,19 @@ export const SETTINGS_SCHEMA = {
|
||||
description: 'Display memory usage information in the UI',
|
||||
showInDialog: true,
|
||||
},
|
||||
|
||||
usageStatisticsEnabled: {
|
||||
showLineNumbers: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Usage Statistics',
|
||||
category: 'General',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable collection of usage statistics',
|
||||
showInDialog: false, // All details are shown in /privacy and dependent on auth type
|
||||
},
|
||||
autoConfigureMaxOldSpaceSize: {
|
||||
type: 'boolean',
|
||||
label: 'Auto Configure Max Old Space Size',
|
||||
category: 'General',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Automatically configure Node.js memory limits',
|
||||
showInDialog: true,
|
||||
},
|
||||
preferredEditor: {
|
||||
type: 'string',
|
||||
label: 'Preferred Editor',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
description: 'The preferred editor to open files in.',
|
||||
showInDialog: false,
|
||||
},
|
||||
maxSessionTurns: {
|
||||
type: 'number',
|
||||
label: 'Max Session Turns',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: -1,
|
||||
description:
|
||||
'Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.',
|
||||
showInDialog: true,
|
||||
},
|
||||
memoryImportFormat: {
|
||||
type: 'string',
|
||||
label: 'Memory Import Format',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: undefined as MemoryImportFormat | undefined,
|
||||
description: 'The format to use when importing memory.',
|
||||
showInDialog: false,
|
||||
},
|
||||
memoryDiscoveryMaxDirs: {
|
||||
type: 'number',
|
||||
label: 'Memory Discovery Max Dirs',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: 200,
|
||||
description: 'Maximum number of directories to search for memory.',
|
||||
showInDialog: true,
|
||||
},
|
||||
contextFileName: {
|
||||
type: 'object',
|
||||
label: 'Context File Name',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | string[] | undefined,
|
||||
description: 'The name of the context file.',
|
||||
showInDialog: false,
|
||||
},
|
||||
vimMode: {
|
||||
type: 'boolean',
|
||||
label: 'Vim Mode',
|
||||
category: 'Mode',
|
||||
label: 'Show Line Numbers',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Enable Vim keybindings',
|
||||
description: 'Show line numbers in the chat.',
|
||||
showInDialog: true,
|
||||
},
|
||||
ideMode: {
|
||||
type: 'boolean',
|
||||
label: 'IDE Mode',
|
||||
category: 'Mode',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable IDE integration mode',
|
||||
showInDialog: true,
|
||||
},
|
||||
|
||||
accessibility: {
|
||||
type: 'object',
|
||||
label: 'Accessibility',
|
||||
category: 'Accessibility',
|
||||
category: 'UI',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Accessibility settings.',
|
||||
@@ -200,7 +231,7 @@ export const SETTINGS_SCHEMA = {
|
||||
disableLoadingPhrases: {
|
||||
type: 'boolean',
|
||||
label: 'Disable Loading Phrases',
|
||||
category: 'Accessibility',
|
||||
category: 'UI',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Disable loading phrases for accessibility',
|
||||
@@ -209,7 +240,7 @@ export const SETTINGS_SCHEMA = {
|
||||
screenReader: {
|
||||
type: 'boolean',
|
||||
label: 'Screen Reader Mode',
|
||||
category: 'Accessibility',
|
||||
category: 'UI',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
@@ -218,30 +249,189 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
checkpointing: {
|
||||
},
|
||||
},
|
||||
|
||||
ide: {
|
||||
type: 'object',
|
||||
label: 'Checkpointing',
|
||||
category: 'Checkpointing',
|
||||
label: 'IDE',
|
||||
category: 'IDE',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Session checkpointing settings.',
|
||||
description: 'IDE integration settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Checkpointing',
|
||||
category: 'Checkpointing',
|
||||
label: 'IDE Mode',
|
||||
category: 'IDE',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable session checkpointing for recovery',
|
||||
description: 'Enable IDE integration mode',
|
||||
showInDialog: true,
|
||||
},
|
||||
hasSeenNudge: {
|
||||
type: 'boolean',
|
||||
label: 'Has Seen IDE Integration Nudge',
|
||||
category: 'IDE',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Whether the user has seen the IDE integration nudge.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
privacy: {
|
||||
type: 'object',
|
||||
label: 'Privacy',
|
||||
category: 'Privacy',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Privacy-related settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
usageStatisticsEnabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Usage Statistics',
|
||||
category: 'Privacy',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable collection of usage statistics',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
telemetry: {
|
||||
type: 'object',
|
||||
label: 'Telemetry',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as TelemetrySettings | undefined,
|
||||
description: 'Telemetry configuration.',
|
||||
showInDialog: false,
|
||||
},
|
||||
|
||||
model: {
|
||||
type: 'object',
|
||||
label: 'Model',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'Settings related to the generative model.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
label: 'Model',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
description: 'The Gemini model to use for conversations.',
|
||||
showInDialog: false,
|
||||
},
|
||||
maxSessionTurns: {
|
||||
type: 'number',
|
||||
label: 'Max Session Turns',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: -1,
|
||||
description:
|
||||
'Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.',
|
||||
showInDialog: true,
|
||||
},
|
||||
summarizeToolOutput: {
|
||||
type: 'object',
|
||||
label: 'Summarize Tool Output',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: undefined as
|
||||
| Record<string, { tokenBudget?: number }>
|
||||
| undefined,
|
||||
description: 'Settings for summarizing tool output.',
|
||||
showInDialog: false,
|
||||
},
|
||||
chatCompression: {
|
||||
type: 'object',
|
||||
label: 'Chat Compression',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: undefined as ChatCompressionSettings | undefined,
|
||||
description: 'Chat compression settings.',
|
||||
showInDialog: false,
|
||||
},
|
||||
skipNextSpeakerCheck: {
|
||||
type: 'boolean',
|
||||
label: 'Skip Next Speaker Check',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Skip the next speaker check.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
context: {
|
||||
type: 'object',
|
||||
label: 'Context',
|
||||
category: 'Context',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'Settings for managing context provided to the model.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
fileName: {
|
||||
type: 'object',
|
||||
label: 'Context File Name',
|
||||
category: 'Context',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | string[] | undefined,
|
||||
description: 'The name of the context file.',
|
||||
showInDialog: false,
|
||||
},
|
||||
importFormat: {
|
||||
type: 'string',
|
||||
label: 'Memory Import Format',
|
||||
category: 'Context',
|
||||
requiresRestart: false,
|
||||
default: undefined as MemoryImportFormat | undefined,
|
||||
description: 'The format to use when importing memory.',
|
||||
showInDialog: false,
|
||||
},
|
||||
discoveryMaxDirs: {
|
||||
type: 'number',
|
||||
label: 'Memory Discovery Max Dirs',
|
||||
category: 'Context',
|
||||
requiresRestart: false,
|
||||
default: 200,
|
||||
description: 'Maximum number of directories to search for memory.',
|
||||
showInDialog: true,
|
||||
},
|
||||
includeDirectories: {
|
||||
type: 'array',
|
||||
label: 'Include Directories',
|
||||
category: 'Context',
|
||||
requiresRestart: false,
|
||||
default: [] as string[],
|
||||
description:
|
||||
'Additional directories to include in the workspace context. Missing directories will be skipped with a warning.',
|
||||
showInDialog: false,
|
||||
},
|
||||
loadMemoryFromIncludeDirectories: {
|
||||
type: 'boolean',
|
||||
label: 'Load Memory From Include Directories',
|
||||
category: 'Context',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Whether to load memory files from include directories.',
|
||||
showInDialog: true,
|
||||
},
|
||||
fileFiltering: {
|
||||
type: 'object',
|
||||
label: 'File Filtering',
|
||||
category: 'File Filtering',
|
||||
category: 'Context',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Settings for git-aware file filtering.',
|
||||
@@ -250,7 +440,7 @@ export const SETTINGS_SCHEMA = {
|
||||
respectGitIgnore: {
|
||||
type: 'boolean',
|
||||
label: 'Respect .gitignore',
|
||||
category: 'File Filtering',
|
||||
category: 'Context',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Respect .gitignore files when searching',
|
||||
@@ -259,7 +449,7 @@ export const SETTINGS_SCHEMA = {
|
||||
respectGeminiIgnore: {
|
||||
type: 'boolean',
|
||||
label: 'Respect .geminiignore',
|
||||
category: 'File Filtering',
|
||||
category: 'Context',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Respect .geminiignore files when searching',
|
||||
@@ -268,7 +458,7 @@ export const SETTINGS_SCHEMA = {
|
||||
enableRecursiveFileSearch: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Recursive File Search',
|
||||
category: 'File Filtering',
|
||||
category: 'Context',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable recursive file search functionality',
|
||||
@@ -277,7 +467,7 @@ export const SETTINGS_SCHEMA = {
|
||||
disableFuzzySearch: {
|
||||
type: 'boolean',
|
||||
label: 'Disable Fuzzy Search',
|
||||
category: 'File Filtering',
|
||||
category: 'Context',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Disable fuzzy search when searching for files.',
|
||||
@@ -285,66 +475,48 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
disableAutoUpdate: {
|
||||
type: 'boolean',
|
||||
label: 'Disable Auto Update',
|
||||
category: 'Updates',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Disable automatic updates',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
|
||||
shouldUseNodePtyShell: {
|
||||
type: 'boolean',
|
||||
label: 'Use node-pty for Shell Execution',
|
||||
category: 'Shell',
|
||||
tools: {
|
||||
type: 'object',
|
||||
label: 'Tools',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Use node-pty for shell command execution. Fallback to child_process still applies.',
|
||||
showInDialog: true,
|
||||
},
|
||||
|
||||
selectedAuthType: {
|
||||
type: 'string',
|
||||
label: 'Selected Auth Type',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as AuthType | undefined,
|
||||
description: 'The currently selected authentication type.',
|
||||
default: {},
|
||||
description: 'Settings for built-in and custom tools.',
|
||||
showInDialog: false,
|
||||
},
|
||||
useExternalAuth: {
|
||||
type: 'boolean',
|
||||
label: 'Use External Auth',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as boolean | undefined,
|
||||
description: 'Whether to use an external authentication flow.',
|
||||
showInDialog: false,
|
||||
},
|
||||
properties: {
|
||||
sandbox: {
|
||||
type: 'object',
|
||||
label: 'Sandbox',
|
||||
category: 'Advanced',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: undefined as boolean | string | undefined,
|
||||
description:
|
||||
'Sandbox execution environment (can be a boolean or a path string).',
|
||||
showInDialog: false,
|
||||
},
|
||||
coreTools: {
|
||||
usePty: {
|
||||
type: 'boolean',
|
||||
label: 'Use node-pty for Shell Execution',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Use node-pty for shell command execution. Fallback to child_process still applies.',
|
||||
showInDialog: true,
|
||||
},
|
||||
core: {
|
||||
type: 'array',
|
||||
label: 'Core Tools',
|
||||
category: 'Advanced',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: undefined as string[] | undefined,
|
||||
description: 'Paths to core tool definitions.',
|
||||
showInDialog: false,
|
||||
},
|
||||
allowedTools: {
|
||||
allowed: {
|
||||
type: 'array',
|
||||
label: 'Allowed Tools',
|
||||
category: 'Advanced',
|
||||
@@ -354,76 +526,189 @@ export const SETTINGS_SCHEMA = {
|
||||
'A list of tool names that will bypass the confirmation dialog.',
|
||||
showInDialog: false,
|
||||
},
|
||||
excludeTools: {
|
||||
exclude: {
|
||||
type: 'array',
|
||||
label: 'Exclude Tools',
|
||||
category: 'Advanced',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: undefined as string[] | undefined,
|
||||
description: 'Tool names to exclude from discovery.',
|
||||
showInDialog: false,
|
||||
},
|
||||
toolDiscoveryCommand: {
|
||||
discoveryCommand: {
|
||||
type: 'string',
|
||||
label: 'Tool Discovery Command',
|
||||
category: 'Advanced',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description: 'Command to run for tool discovery.',
|
||||
showInDialog: false,
|
||||
},
|
||||
toolCallCommand: {
|
||||
callCommand: {
|
||||
type: 'string',
|
||||
label: 'Tool Call Command',
|
||||
category: 'Advanced',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description: 'Command to run for tool calls.',
|
||||
showInDialog: false,
|
||||
},
|
||||
mcpServerCommand: {
|
||||
useRipgrep: {
|
||||
type: 'boolean',
|
||||
label: 'Use Ripgrep',
|
||||
category: 'Tools',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
mcp: {
|
||||
type: 'object',
|
||||
label: 'MCP',
|
||||
category: 'MCP',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Settings for Model Context Protocol (MCP) servers.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
serverCommand: {
|
||||
type: 'string',
|
||||
label: 'MCP Server Command',
|
||||
category: 'Advanced',
|
||||
category: 'MCP',
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description: 'Command to start an MCP server.',
|
||||
showInDialog: false,
|
||||
},
|
||||
mcpServers: {
|
||||
type: 'object',
|
||||
label: 'MCP Servers',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: {} as Record<string, MCPServerConfig>,
|
||||
description: 'Configuration for MCP servers.',
|
||||
showInDialog: false,
|
||||
},
|
||||
allowMCPServers: {
|
||||
allowed: {
|
||||
type: 'array',
|
||||
label: 'Allow MCP Servers',
|
||||
category: 'Advanced',
|
||||
category: 'MCP',
|
||||
requiresRestart: true,
|
||||
default: undefined as string[] | undefined,
|
||||
description: 'A whitelist of MCP servers to allow.',
|
||||
showInDialog: false,
|
||||
},
|
||||
excludeMCPServers: {
|
||||
excluded: {
|
||||
type: 'array',
|
||||
label: 'Exclude MCP Servers',
|
||||
category: 'Advanced',
|
||||
category: 'MCP',
|
||||
requiresRestart: true,
|
||||
default: undefined as string[] | undefined,
|
||||
description: 'A blacklist of MCP servers to exclude.',
|
||||
showInDialog: false,
|
||||
},
|
||||
telemetry: {
|
||||
},
|
||||
},
|
||||
|
||||
security: {
|
||||
type: 'object',
|
||||
label: 'Telemetry',
|
||||
label: 'Security',
|
||||
category: 'Security',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Security-related settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
folderTrust: {
|
||||
type: 'object',
|
||||
label: 'Folder Trust',
|
||||
category: 'Security',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'Settings for folder trust.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
featureEnabled: {
|
||||
type: 'boolean',
|
||||
label: 'Folder Trust Feature',
|
||||
category: 'Security',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Enable folder trust feature for enhanced security.',
|
||||
showInDialog: true,
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Folder Trust',
|
||||
category: 'Security',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Setting to track whether Folder trust is enabled.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
type: 'object',
|
||||
label: 'Authentication',
|
||||
category: 'Security',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Authentication settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
selectedType: {
|
||||
type: 'string',
|
||||
label: 'Selected Auth Type',
|
||||
category: 'Security',
|
||||
requiresRestart: true,
|
||||
default: undefined as AuthType | undefined,
|
||||
description: 'The currently selected authentication type.',
|
||||
showInDialog: false,
|
||||
},
|
||||
useExternal: {
|
||||
type: 'boolean',
|
||||
label: 'Use External Auth',
|
||||
category: 'Security',
|
||||
requiresRestart: true,
|
||||
default: undefined as boolean | undefined,
|
||||
description: 'Whether to use an external authentication flow.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
advanced: {
|
||||
type: 'object',
|
||||
label: 'Advanced',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as TelemetrySettings | undefined,
|
||||
description: 'Telemetry configuration.',
|
||||
default: {},
|
||||
description: 'Advanced settings for power users.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
autoConfigureMemory: {
|
||||
type: 'boolean',
|
||||
label: 'Auto Configure Max Old Space Size',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Automatically configure Node.js memory limits',
|
||||
showInDialog: false,
|
||||
},
|
||||
dnsResolutionOrder: {
|
||||
type: 'string',
|
||||
label: 'DNS Resolution Order',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as DnsResolutionOrder | undefined,
|
||||
description: 'The DNS resolution order.',
|
||||
showInDialog: false,
|
||||
},
|
||||
excludedEnvVars: {
|
||||
type: 'array',
|
||||
label: 'Excluded Project Environment Variables',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: ['DEBUG', 'DEBUG_MODE'] as string[],
|
||||
description: 'Environment variables to exclude from project context.',
|
||||
showInDialog: false,
|
||||
},
|
||||
bugCommand: {
|
||||
@@ -435,125 +720,30 @@ export const SETTINGS_SCHEMA = {
|
||||
description: 'Configuration for the bug report command.',
|
||||
showInDialog: false,
|
||||
},
|
||||
summarizeToolOutput: {
|
||||
type: 'object',
|
||||
label: 'Summarize Tool Output',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: undefined as Record<string, { tokenBudget?: number }> | undefined,
|
||||
description: 'Settings for summarizing tool output.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
|
||||
dnsResolutionOrder: {
|
||||
type: 'string',
|
||||
label: 'DNS Resolution Order',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as DnsResolutionOrder | undefined,
|
||||
description: 'The DNS resolution order.',
|
||||
showInDialog: false,
|
||||
},
|
||||
excludedProjectEnvVars: {
|
||||
type: 'array',
|
||||
label: 'Excluded Project Environment Variables',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: ['DEBUG', 'DEBUG_MODE'] as string[],
|
||||
description: 'Environment variables to exclude from project context.',
|
||||
showInDialog: false,
|
||||
},
|
||||
disableUpdateNag: {
|
||||
type: 'boolean',
|
||||
label: 'Disable Update Nag',
|
||||
category: 'Updates',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Disable update notification prompts.',
|
||||
showInDialog: false,
|
||||
},
|
||||
includeDirectories: {
|
||||
type: 'array',
|
||||
label: 'Include Directories',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: [] as string[],
|
||||
description:
|
||||
'Additional directories to include in the workspace context. Missing directories will be skipped with a warning.',
|
||||
showInDialog: false,
|
||||
},
|
||||
loadMemoryFromIncludeDirectories: {
|
||||
type: 'boolean',
|
||||
label: 'Load Memory From Include Directories',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Whether to load memory files from include directories.',
|
||||
showInDialog: true,
|
||||
},
|
||||
model: {
|
||||
type: 'string',
|
||||
label: 'Model',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
description: 'The Gemini model to use for conversations.',
|
||||
showInDialog: false,
|
||||
},
|
||||
hasSeenIdeIntegrationNudge: {
|
||||
type: 'boolean',
|
||||
label: 'Has Seen IDE Integration Nudge',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Whether the user has seen the IDE integration nudge.',
|
||||
showInDialog: false,
|
||||
},
|
||||
folderTrustFeature: {
|
||||
type: 'boolean',
|
||||
label: 'Folder Trust Feature',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Enable folder trust feature for enhanced security.',
|
||||
showInDialog: true,
|
||||
},
|
||||
folderTrust: {
|
||||
type: 'boolean',
|
||||
label: 'Folder Trust',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Setting to track whether Folder trust is enabled.',
|
||||
showInDialog: true,
|
||||
},
|
||||
chatCompression: {
|
||||
experimental: {
|
||||
type: 'object',
|
||||
label: 'Chat Compression',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: undefined as ChatCompressionSettings | undefined,
|
||||
description: 'Chat compression settings.',
|
||||
label: 'Experimental',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Setting to enable experimental features',
|
||||
showInDialog: false,
|
||||
},
|
||||
showLineNumbers: {
|
||||
type: 'boolean',
|
||||
label: 'Show Line Numbers',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Show line numbers in the chat.',
|
||||
showInDialog: true,
|
||||
},
|
||||
properties: {
|
||||
extensionManagement: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Management',
|
||||
category: 'Feature Flag',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable extension management features.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
extensions: {
|
||||
type: 'object',
|
||||
label: 'Extensions',
|
||||
@@ -584,44 +774,6 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
skipNextSpeakerCheck: {
|
||||
type: 'boolean',
|
||||
label: 'Skip Next Speaker Check',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Skip the next speaker check.',
|
||||
showInDialog: true,
|
||||
},
|
||||
useRipgrep: {
|
||||
type: 'boolean',
|
||||
label: 'Use Ripgrep',
|
||||
category: 'Tools',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.',
|
||||
showInDialog: true,
|
||||
},
|
||||
enablePromptCompletion: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Prompt Completion',
|
||||
category: 'General',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable AI-powered prompt completion suggestions while typing.',
|
||||
showInDialog: true,
|
||||
},
|
||||
debugKeystrokeLogging: {
|
||||
type: 'boolean',
|
||||
label: 'Debug Keystroke Logging',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Enable debug logging of keystrokes to the console.',
|
||||
showInDialog: true,
|
||||
},
|
||||
} as const;
|
||||
|
||||
type InferSettings<T extends SettingsSchema> = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -152,6 +152,7 @@ describe('gemini.tsx main function', () => {
|
||||
workspaceSettingsFile,
|
||||
[settingsError],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
loadSettingsMock.mockReturnValue(mockLoadedSettings);
|
||||
@@ -241,8 +242,10 @@ describe('startInteractiveUI', () => {
|
||||
} as Config;
|
||||
const mockSettings = {
|
||||
merged: {
|
||||
ui: {
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
},
|
||||
} as LoadedSettings;
|
||||
const mockStartupWarnings = ['warning1'];
|
||||
const mockWorkspaceRoot = '/root';
|
||||
|
||||
@@ -199,7 +199,7 @@ export async function main() {
|
||||
registerCleanup(consolePatcher.cleanup);
|
||||
|
||||
dns.setDefaultResultOrder(
|
||||
validateDnsResolutionOrder(settings.merged.dnsResolutionOrder),
|
||||
validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),
|
||||
);
|
||||
|
||||
if (argv.promptInteractive && !process.stdin.isTTY) {
|
||||
@@ -218,7 +218,7 @@ export async function main() {
|
||||
}
|
||||
|
||||
// Set a default auth type if one isn't set.
|
||||
if (!settings.merged.selectedAuthType) {
|
||||
if (!settings.merged.security?.auth?.selectedType) {
|
||||
if (process.env['CLOUD_SHELL'] === 'true') {
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
@@ -246,34 +246,36 @@ export async function main() {
|
||||
}
|
||||
|
||||
// Load custom themes from settings
|
||||
themeManager.loadCustomThemes(settings.merged.customThemes);
|
||||
themeManager.loadCustomThemes(settings.merged.ui?.customThemes);
|
||||
|
||||
if (settings.merged.theme) {
|
||||
if (!themeManager.setActiveTheme(settings.merged.theme)) {
|
||||
if (settings.merged.ui?.theme) {
|
||||
if (!themeManager.setActiveTheme(settings.merged.ui?.theme)) {
|
||||
// If the theme is not found during initial load, log a warning and continue.
|
||||
// The useThemeCommand hook in App.tsx will handle opening the dialog.
|
||||
console.warn(`Warning: Theme "${settings.merged.theme}" not found.`);
|
||||
console.warn(`Warning: Theme "${settings.merged.ui?.theme}" not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
// hop into sandbox if we are outside and sandboxing is enabled
|
||||
if (!process.env['SANDBOX']) {
|
||||
const memoryArgs = settings.merged.autoConfigureMaxOldSpaceSize
|
||||
const memoryArgs = settings.merged.advanced?.autoConfigureMemory
|
||||
? getNodeMemoryArgs(config)
|
||||
: [];
|
||||
const sandboxConfig = config.getSandbox();
|
||||
if (sandboxConfig) {
|
||||
if (
|
||||
settings.merged.selectedAuthType &&
|
||||
!settings.merged.useExternalAuth
|
||||
settings.merged.security?.auth?.selectedType &&
|
||||
!settings.merged.security?.auth?.useExternal
|
||||
) {
|
||||
// Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.
|
||||
try {
|
||||
const err = validateAuthMethod(settings.merged.selectedAuthType);
|
||||
const err = validateAuthMethod(
|
||||
settings.merged.security.auth.selectedType,
|
||||
);
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
await config.refreshAuth(settings.merged.selectedAuthType);
|
||||
await config.refreshAuth(settings.merged.security.auth.selectedType);
|
||||
} catch (err) {
|
||||
console.error('Error authenticating:', err);
|
||||
process.exit(1);
|
||||
@@ -322,11 +324,12 @@ export async function main() {
|
||||
}
|
||||
|
||||
if (
|
||||
settings.merged.selectedAuthType === AuthType.LOGIN_WITH_GOOGLE &&
|
||||
settings.merged.security?.auth?.selectedType ===
|
||||
AuthType.LOGIN_WITH_GOOGLE &&
|
||||
config.isBrowserLaunchSuppressed()
|
||||
) {
|
||||
// Do oauth before app renders to make copying the link possible.
|
||||
await getOauthClient(settings.merged.selectedAuthType, config);
|
||||
await getOauthClient(settings.merged.security.auth.selectedType, config);
|
||||
}
|
||||
|
||||
if (config.getExperimentalZedIntegration()) {
|
||||
@@ -370,8 +373,8 @@ export async function main() {
|
||||
});
|
||||
|
||||
const nonInteractiveConfig = await validateNonInteractiveAuth(
|
||||
settings.merged.selectedAuthType,
|
||||
settings.merged.useExternalAuth,
|
||||
settings.merged.security?.auth?.selectedType,
|
||||
settings.merged.security?.auth?.useExternal,
|
||||
config,
|
||||
);
|
||||
|
||||
@@ -384,7 +387,7 @@ export async function main() {
|
||||
}
|
||||
|
||||
function setWindowTitle(title: string, settings: LoadedSettings) {
|
||||
if (!settings.merged.hideWindowTitle) {
|
||||
if (!settings.merged.ui?.hideWindowTitle) {
|
||||
const windowTitle = (
|
||||
process.env['CLI_TITLE'] || `Gemini - ${title}`
|
||||
).replace(
|
||||
|
||||
@@ -313,6 +313,7 @@ describe('App UI', () => {
|
||||
workspaceSettingsFile,
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -684,7 +685,10 @@ describe('App UI', () => {
|
||||
|
||||
it('should display custom contextFileName in footer when set and count is 1', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
workspace: { contextFileName: 'AGENTS.md', theme: 'Default' },
|
||||
workspace: {
|
||||
context: { fileName: 'AGENTS.md' },
|
||||
ui: { theme: 'Default' },
|
||||
},
|
||||
});
|
||||
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
|
||||
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['AGENTS.md']);
|
||||
@@ -706,8 +710,8 @@ describe('App UI', () => {
|
||||
it('should display a generic message when multiple context files with different names are provided', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
workspace: {
|
||||
contextFileName: ['AGENTS.md', 'CONTEXT.md'],
|
||||
theme: 'Default',
|
||||
context: { fileName: ['AGENTS.md', 'CONTEXT.md'] },
|
||||
ui: { theme: 'Default' },
|
||||
},
|
||||
});
|
||||
mockConfig.getGeminiMdFileCount.mockReturnValue(2);
|
||||
@@ -732,7 +736,10 @@ describe('App UI', () => {
|
||||
|
||||
it('should display custom contextFileName with plural when set and count is > 1', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
workspace: { contextFileName: 'MY_NOTES.TXT', theme: 'Default' },
|
||||
workspace: {
|
||||
context: { fileName: 'MY_NOTES.TXT' },
|
||||
ui: { theme: 'Default' },
|
||||
},
|
||||
});
|
||||
mockConfig.getGeminiMdFileCount.mockReturnValue(3);
|
||||
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
|
||||
@@ -757,7 +764,10 @@ describe('App UI', () => {
|
||||
|
||||
it('should not display context file message if count is 0, even if contextFileName is set', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
workspace: { contextFileName: 'ANY_FILE.MD', theme: 'Default' },
|
||||
workspace: {
|
||||
context: { fileName: 'ANY_FILE.MD' },
|
||||
ui: { theme: 'Default' },
|
||||
},
|
||||
});
|
||||
mockConfig.getGeminiMdFileCount.mockReturnValue(0);
|
||||
mockConfig.getAllGeminiMdFilenames.mockReturnValue([]);
|
||||
@@ -838,7 +848,7 @@ describe('App UI', () => {
|
||||
it('should not display Tips component when hideTips is true', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
workspace: {
|
||||
hideTips: true,
|
||||
ui: { hideTips: true },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -871,7 +881,7 @@ describe('App UI', () => {
|
||||
it('should not display Header component when hideBanner is true', async () => {
|
||||
const { Header } = await import('./components/Header.js');
|
||||
mockSettings = createMockSettings({
|
||||
user: { hideBanner: true },
|
||||
user: { ui: { hideBanner: true } },
|
||||
});
|
||||
|
||||
const { unmount } = renderWithProviders(
|
||||
@@ -902,7 +912,7 @@ describe('App UI', () => {
|
||||
|
||||
it('should not display Footer component when hideFooter is true', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
user: { hideFooter: true },
|
||||
user: { ui: { hideFooter: true } },
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
@@ -920,9 +930,9 @@ describe('App UI', () => {
|
||||
|
||||
it('should show footer if system says show, but workspace and user settings say hide', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
system: { hideFooter: false },
|
||||
user: { hideFooter: true },
|
||||
workspace: { hideFooter: true },
|
||||
system: { ui: { hideFooter: false } },
|
||||
user: { ui: { hideFooter: true } },
|
||||
workspace: { ui: { hideFooter: true } },
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
@@ -940,9 +950,9 @@ describe('App UI', () => {
|
||||
|
||||
it('should show tips if system says show, but workspace and user settings say hide', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
system: { hideTips: false },
|
||||
user: { hideTips: true },
|
||||
workspace: { hideTips: true },
|
||||
system: { ui: { hideTips: false } },
|
||||
user: { ui: { hideTips: true } },
|
||||
workspace: { ui: { hideTips: true } },
|
||||
});
|
||||
|
||||
const { unmount } = renderWithProviders(
|
||||
@@ -1117,9 +1127,13 @@ describe('App UI', () => {
|
||||
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
|
||||
mockSettings = createMockSettings({
|
||||
workspace: {
|
||||
selectedAuthType: 'USE_GEMINI' as AuthType,
|
||||
useExternalAuth: false,
|
||||
theme: 'Default',
|
||||
security: {
|
||||
auth: {
|
||||
selectedType: 'USE_GEMINI' as AuthType,
|
||||
useExternal: false,
|
||||
},
|
||||
},
|
||||
ui: { theme: 'Default' },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1139,9 +1153,13 @@ describe('App UI', () => {
|
||||
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
|
||||
mockSettings = createMockSettings({
|
||||
workspace: {
|
||||
selectedAuthType: 'USE_GEMINI' as AuthType,
|
||||
useExternalAuth: true,
|
||||
theme: 'Default',
|
||||
security: {
|
||||
auth: {
|
||||
selectedType: 'USE_GEMINI' as AuthType,
|
||||
useExternal: true,
|
||||
},
|
||||
},
|
||||
ui: { theme: 'Default' },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1536,8 +1554,8 @@ describe('App UI', () => {
|
||||
it('should pass debugKeystrokeLogging setting to KeypressProvider', () => {
|
||||
const mockSettingsWithDebug = createMockSettings({
|
||||
workspace: {
|
||||
theme: 'Default',
|
||||
debugKeystrokeLogging: true,
|
||||
ui: { theme: 'Default' },
|
||||
advanced: { debugKeystrokeLogging: true },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1553,7 +1571,9 @@ describe('App UI', () => {
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toBeDefined();
|
||||
expect(mockSettingsWithDebug.merged.debugKeystrokeLogging).toBe(true);
|
||||
expect(mockSettingsWithDebug.merged.advanced?.debugKeystrokeLogging).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default false value when debugKeystrokeLogging is not set', () => {
|
||||
@@ -1569,7 +1589,9 @@ describe('App UI', () => {
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toBeDefined();
|
||||
expect(mockSettings.merged.debugKeystrokeLogging).toBeUndefined();
|
||||
expect(
|
||||
mockSettings.merged.advanced?.debugKeystrokeLogging,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -134,7 +134,9 @@ export const AppWrapper = (props: AppProps) => {
|
||||
<KeypressProvider
|
||||
kittyProtocolEnabled={kittyProtocolStatus.enabled}
|
||||
config={props.config}
|
||||
debugKeystrokeLogging={props.settings.merged.debugKeystrokeLogging}
|
||||
debugKeystrokeLogging={
|
||||
props.settings.merged.general?.debugKeystrokeLogging
|
||||
}
|
||||
>
|
||||
<SessionStatsProvider>
|
||||
<VimModeProvider settings={props.settings}>
|
||||
@@ -161,7 +163,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
const shouldShowIdePrompt =
|
||||
currentIDE &&
|
||||
!config.getIdeMode() &&
|
||||
!settings.merged.hasSeenIdeIntegrationNudge &&
|
||||
!settings.merged.ide?.hasSeenNudge &&
|
||||
!idePromptAnswered;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -301,16 +303,21 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
} = useAuthCommand(settings, setAuthError, config);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings.merged.selectedAuthType && !settings.merged.useExternalAuth) {
|
||||
const error = validateAuthMethod(settings.merged.selectedAuthType);
|
||||
if (
|
||||
settings.merged.security?.auth?.selectedType &&
|
||||
!settings.merged.security?.auth?.useExternal
|
||||
) {
|
||||
const error = validateAuthMethod(
|
||||
settings.merged.security.auth.selectedType,
|
||||
);
|
||||
if (error) {
|
||||
setAuthError(error);
|
||||
openAuthDialog();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
settings.merged.selectedAuthType,
|
||||
settings.merged.useExternalAuth,
|
||||
settings.merged.security?.auth?.selectedType,
|
||||
settings.merged.security?.auth?.useExternal,
|
||||
openAuthDialog,
|
||||
setAuthError,
|
||||
]);
|
||||
@@ -345,14 +352,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
try {
|
||||
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
|
||||
process.cwd(),
|
||||
settings.merged.loadMemoryFromIncludeDirectories
|
||||
settings.merged.context?.loadMemoryFromIncludeDirectories
|
||||
? config.getWorkspaceContext().getDirectories()
|
||||
: [],
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
settings.merged,
|
||||
config.getExtensionContextFilePaths(),
|
||||
settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree'
|
||||
settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree'
|
||||
config.getFileFilteringOptions(),
|
||||
);
|
||||
|
||||
@@ -510,7 +517,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
}, []);
|
||||
|
||||
const getPreferredEditor = useCallback(() => {
|
||||
const editorType = settings.merged.preferredEditor;
|
||||
const editorType = settings.merged.general?.preferredEditor;
|
||||
const isValidEditor = isEditorAvailable(editorType);
|
||||
if (!isValidEditor) {
|
||||
openEditorDialog();
|
||||
@@ -701,7 +708,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
const handleGlobalKeypress = useCallback(
|
||||
(key: Key) => {
|
||||
// Debug log keystrokes if enabled
|
||||
if (settings.merged.debugKeystrokeLogging) {
|
||||
if (settings.merged.general?.debugKeystrokeLogging) {
|
||||
console.log('[DEBUG] Keystroke:', JSON.stringify(key));
|
||||
}
|
||||
|
||||
@@ -768,7 +775,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
handleSlashCommand,
|
||||
isAuthenticating,
|
||||
cancelOngoingRequest,
|
||||
settings.merged.debugKeystrokeLogging,
|
||||
settings.merged.general?.debugKeystrokeLogging,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -884,12 +891,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
const branchName = useGitBranchName(config.getTargetDir());
|
||||
|
||||
const contextFileNames = useMemo(() => {
|
||||
const fromSettings = settings.merged.contextFileName;
|
||||
const fromSettings = settings.merged.context?.fileName;
|
||||
if (fromSettings) {
|
||||
return Array.isArray(fromSettings) ? fromSettings : [fromSettings];
|
||||
}
|
||||
return getAllGeminiMdFilenames();
|
||||
}, [settings.merged.contextFileName]);
|
||||
}, [settings.merged.context?.fileName]);
|
||||
|
||||
const initialPrompt = useMemo(() => config.getQuestion(), [config]);
|
||||
const geminiClient = config.getGeminiClient();
|
||||
@@ -965,10 +972,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
key={staticKey}
|
||||
items={[
|
||||
<Box flexDirection="column" key="header">
|
||||
{!(settings.merged.hideBanner || config.getScreenReader()) && (
|
||||
<Header version={version} nightly={nightly} />
|
||||
)}
|
||||
{!(settings.merged.hideTips || config.getScreenReader()) && (
|
||||
{!(
|
||||
settings.merged.ui?.hideBanner || config.getScreenReader()
|
||||
) && <Header version={version} nightly={nightly} />}
|
||||
{!(settings.merged.ui?.hideTips || config.getScreenReader()) && (
|
||||
<Tips config={config} />
|
||||
)}
|
||||
</Box>,
|
||||
@@ -1300,7 +1307,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{!settings.merged.hideFooter && (
|
||||
{!settings.merged.ui?.hideFooter && (
|
||||
<Footer
|
||||
model={currentModel}
|
||||
targetDir={config.getTargetDir()}
|
||||
@@ -1312,7 +1319,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
||||
showErrorDetails={showErrorDetails}
|
||||
showMemoryUsage={
|
||||
config.getDebugMode() ||
|
||||
settings.merged.showMemoryUsage ||
|
||||
settings.merged.ui?.showMemoryUsage ||
|
||||
false
|
||||
}
|
||||
promptTokenCount={sessionStats.lastPromptTokenCount}
|
||||
|
||||
@@ -32,7 +32,11 @@ describe('aboutCommand', () => {
|
||||
},
|
||||
settings: {
|
||||
merged: {
|
||||
selectedAuthType: 'test-auth',
|
||||
security: {
|
||||
auth: {
|
||||
selectedType: 'test-auth',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ export const aboutCommand: SlashCommand = {
|
||||
const modelVersion = context.services.config?.getModel() || 'Unknown';
|
||||
const cliVersion = await getCliVersion();
|
||||
const selectedAuthType =
|
||||
context.services.settings.merged.selectedAuthType || '';
|
||||
context.services.settings.merged.security?.auth?.selectedType || '';
|
||||
const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || '';
|
||||
const ideClient =
|
||||
(context.services.config?.getIdeMode() &&
|
||||
|
||||
@@ -104,9 +104,10 @@ export const directoryCommand: SlashCommand = {
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionContextFilePaths(),
|
||||
context.services.settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree'
|
||||
context.services.settings.merged.context?.importFormat ||
|
||||
'tree', // Use setting or default to 'tree'
|
||||
config.getFileFilteringOptions(),
|
||||
context.services.settings.merged.memoryDiscoveryMaxDirs,
|
||||
context.services.settings.merged.context?.discoveryMaxDirs,
|
||||
);
|
||||
config.setUserMemory(memoryContent);
|
||||
config.setGeminiMdFileCount(fileCount);
|
||||
|
||||
@@ -187,7 +187,11 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
|
||||
Date.now(),
|
||||
);
|
||||
if (result.success) {
|
||||
context.services.settings.setValue(SettingScope.User, 'ideMode', true);
|
||||
context.services.settings.setValue(
|
||||
SettingScope.User,
|
||||
'ide.enabled',
|
||||
true,
|
||||
);
|
||||
// Poll for up to 5 seconds for the extension to activate.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await config.setIdeModeAndSyncConnection(true);
|
||||
@@ -227,7 +231,11 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
|
||||
description: 'enable IDE integration',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context: CommandContext) => {
|
||||
context.services.settings.setValue(SettingScope.User, 'ideMode', true);
|
||||
context.services.settings.setValue(
|
||||
SettingScope.User,
|
||||
'ide.enabled',
|
||||
true,
|
||||
);
|
||||
await config.setIdeModeAndSyncConnection(true);
|
||||
const { messageType, content } = getIdeStatusMessage(ideClient);
|
||||
context.ui.addItem(
|
||||
@@ -245,7 +253,11 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
|
||||
description: 'disable IDE integration',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context: CommandContext) => {
|
||||
context.services.settings.setValue(SettingScope.User, 'ideMode', false);
|
||||
context.services.settings.setValue(
|
||||
SettingScope.User,
|
||||
'ide.enabled',
|
||||
false,
|
||||
);
|
||||
await config.setIdeModeAndSyncConnection(false);
|
||||
const { messageType, content } = getIdeStatusMessage(ideClient);
|
||||
context.ui.addItem(
|
||||
|
||||
@@ -92,9 +92,10 @@ export const memoryCommand: SlashCommand = {
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionContextFilePaths(),
|
||||
context.services.settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree'
|
||||
context.services.settings.merged.context?.importFormat ||
|
||||
'tree', // Use setting or default to 'tree'
|
||||
config.getFileFilteringOptions(),
|
||||
context.services.settings.merged.memoryDiscoveryMaxDirs,
|
||||
context.services.settings.merged.context?.discoveryMaxDirs,
|
||||
);
|
||||
config.setUserMemory(memoryContent);
|
||||
config.setGeminiMdFileCount(fileCount);
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
@@ -40,16 +40,21 @@ describe('AuthDialog', () => {
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: AuthType.USE_GEMINI,
|
||||
security: {
|
||||
auth: {
|
||||
selectedType: AuthType.USE_GEMINI,
|
||||
},
|
||||
},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
@@ -72,8 +77,8 @@ describe('AuthDialog', () => {
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
@@ -83,15 +88,16 @@ describe('AuthDialog', () => {
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
@@ -110,8 +116,8 @@ describe('AuthDialog', () => {
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
@@ -121,15 +127,16 @@ describe('AuthDialog', () => {
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
@@ -148,8 +155,8 @@ describe('AuthDialog', () => {
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
@@ -159,15 +166,16 @@ describe('AuthDialog', () => {
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
@@ -187,8 +195,8 @@ describe('AuthDialog', () => {
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
@@ -198,15 +206,16 @@ describe('AuthDialog', () => {
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
@@ -221,8 +230,8 @@ describe('AuthDialog', () => {
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
@@ -232,15 +241,16 @@ describe('AuthDialog', () => {
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
@@ -257,8 +267,8 @@ describe('AuthDialog', () => {
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
@@ -268,15 +278,16 @@ describe('AuthDialog', () => {
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
@@ -296,7 +307,7 @@ describe('AuthDialog', () => {
|
||||
const onSelect = vi.fn();
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
@@ -305,18 +316,19 @@ describe('AuthDialog', () => {
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame, stdin, unmount } = renderWithProviders(
|
||||
@@ -340,7 +352,7 @@ describe('AuthDialog', () => {
|
||||
const onSelect = vi.fn();
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
@@ -349,18 +361,19 @@ describe('AuthDialog', () => {
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
security: { auth: { selectedType: undefined } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame, stdin, unmount } = renderWithProviders(
|
||||
@@ -387,7 +400,7 @@ describe('AuthDialog', () => {
|
||||
const onSelect = vi.fn();
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
@@ -396,18 +409,19 @@ describe('AuthDialog', () => {
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: AuthType.USE_GEMINI,
|
||||
customThemes: {},
|
||||
security: { auth: { selectedType: AuthType.LOGIN_WITH_GOOGLE } },
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
|
||||
@@ -83,8 +83,8 @@ export function AuthDialog({
|
||||
];
|
||||
|
||||
const initialAuthIndex = items.findIndex((item) => {
|
||||
if (settings.merged.selectedAuthType) {
|
||||
return item.value === settings.merged.selectedAuthType;
|
||||
if (settings.merged.security?.auth?.selectedType) {
|
||||
return item.value === settings.merged.security.auth.selectedType;
|
||||
}
|
||||
|
||||
const defaultAuthType = parseDefaultAuthType(
|
||||
@@ -119,7 +119,7 @@ export function AuthDialog({
|
||||
if (errorMessage) {
|
||||
return;
|
||||
}
|
||||
if (settings.merged.selectedAuthType === undefined) {
|
||||
if (settings.merged.security?.auth?.selectedType === undefined) {
|
||||
// Prevent exiting if no auth method is set
|
||||
setErrorMessage(
|
||||
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
||||
|
||||
@@ -53,7 +53,7 @@ export function EditorSettingsDialog({
|
||||
editorSettingsManager.getAvailableEditorDisplays();
|
||||
|
||||
const currentPreference =
|
||||
settings.forScope(selectedScope).settings.preferredEditor;
|
||||
settings.forScope(selectedScope).settings.general?.preferredEditor;
|
||||
let editorIndex = currentPreference
|
||||
? editorItems.findIndex(
|
||||
(item: EditorDisplay) => item.type === currentPreference,
|
||||
@@ -87,20 +87,26 @@ export function EditorSettingsDialog({
|
||||
selectedScope === SettingScope.User
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
if (settings.forScope(otherScope).settings.preferredEditor !== undefined) {
|
||||
if (
|
||||
settings.forScope(otherScope).settings.general?.preferredEditor !==
|
||||
undefined
|
||||
) {
|
||||
otherScopeModifiedMessage =
|
||||
settings.forScope(selectedScope).settings.preferredEditor !== undefined
|
||||
settings.forScope(selectedScope).settings.general?.preferredEditor !==
|
||||
undefined
|
||||
? `(Also modified in ${otherScope})`
|
||||
: `(Modified in ${otherScope})`;
|
||||
}
|
||||
|
||||
let mergedEditorName = 'None';
|
||||
if (
|
||||
settings.merged.preferredEditor &&
|
||||
isEditorAvailable(settings.merged.preferredEditor)
|
||||
settings.merged.general?.preferredEditor &&
|
||||
isEditorAvailable(settings.merged.general?.preferredEditor)
|
||||
) {
|
||||
mergedEditorName =
|
||||
EDITOR_DISPLAY_NAMES[settings.merged.preferredEditor as EditorType];
|
||||
EDITOR_DISPLAY_NAMES[
|
||||
settings.merged.general?.preferredEditor as EditorType
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -40,7 +40,7 @@ const createMockSettings = (
|
||||
) =>
|
||||
new LoadedSettings(
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {}, ...systemSettings },
|
||||
settings: { ui: { customThemes: {} }, mcpServers: {}, ...systemSettings },
|
||||
path: '/system/settings.json',
|
||||
},
|
||||
{
|
||||
@@ -49,18 +49,23 @@ const createMockSettings = (
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
customThemes: {},
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
...userSettings,
|
||||
},
|
||||
path: '/user/settings.json',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {}, ...workspaceSettings },
|
||||
settings: {
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
...workspaceSettings,
|
||||
},
|
||||
path: '/workspace/settings.json',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
vi.mock('../contexts/SettingsContext.js', async () => {
|
||||
@@ -156,7 +161,11 @@ describe('SettingsDialog', () => {
|
||||
) =>
|
||||
new LoadedSettings(
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {}, ...systemSettings },
|
||||
settings: {
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
...systemSettings,
|
||||
},
|
||||
path: '/system/settings.json',
|
||||
},
|
||||
{
|
||||
@@ -165,18 +174,23 @@ describe('SettingsDialog', () => {
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
customThemes: {},
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
...userSettings,
|
||||
},
|
||||
path: '/user/settings.json',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {}, ...workspaceSettings },
|
||||
settings: {
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
...workspaceSettings,
|
||||
},
|
||||
path: '/workspace/settings.json',
|
||||
},
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
describe('Initial Rendering', () => {
|
||||
@@ -392,11 +406,11 @@ describe('SettingsDialog', () => {
|
||||
|
||||
// Wait for initial render
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Hide Window Title');
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
// The UI should show the settings section is active and scope section is inactive
|
||||
expect(lastFrame()).toContain('● Hide Window Title'); // Settings section active
|
||||
expect(lastFrame()).toContain('● Vim Mode'); // Settings section active
|
||||
expect(lastFrame()).toContain(' Apply To'); // Scope section inactive
|
||||
|
||||
// This test validates the initial state - scope selection behavior
|
||||
@@ -814,11 +828,11 @@ describe('SettingsDialog', () => {
|
||||
|
||||
// Wait for initial render
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Hide Window Title');
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
// Verify initial state: settings section active, scope section inactive
|
||||
expect(lastFrame()).toContain('● Hide Window Title'); // Settings section active
|
||||
expect(lastFrame()).toContain('● Vim Mode'); // Settings section active
|
||||
expect(lastFrame()).toContain(' Apply To'); // Scope section inactive
|
||||
|
||||
// This test validates the rendered UI structure for tab navigation
|
||||
@@ -876,12 +890,12 @@ describe('SettingsDialog', () => {
|
||||
|
||||
// Wait for initial render
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Hide Window Title');
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
// Verify the complete UI is rendered with all necessary sections
|
||||
expect(lastFrame()).toContain('Settings'); // Title
|
||||
expect(lastFrame()).toContain('● Hide Window Title'); // Active setting
|
||||
expect(lastFrame()).toContain('● Vim Mode'); // Active setting
|
||||
expect(lastFrame()).toContain('Apply To'); // Scope section
|
||||
expect(lastFrame()).toContain('1. User Settings'); // Scope options
|
||||
expect(lastFrame()).toContain(
|
||||
|
||||
@@ -153,7 +153,7 @@ export function SettingsDialog({
|
||||
);
|
||||
|
||||
// Special handling for vim mode to sync with VimModeContext
|
||||
if (key === 'vimMode' && newValue !== vimEnabled) {
|
||||
if (key === 'general.vimMode' && newValue !== vimEnabled) {
|
||||
// Call toggleVimEnabled to sync the VimModeContext local state
|
||||
toggleVimEnabled().catch((error) => {
|
||||
console.error('Failed to toggle vim mode:', error);
|
||||
|
||||
@@ -46,13 +46,13 @@ export function ThemeDialog({
|
||||
// Track the currently highlighted theme name
|
||||
const [highlightedThemeName, setHighlightedThemeName] = useState<
|
||||
string | undefined
|
||||
>(settings.merged.theme || DEFAULT_THEME.name);
|
||||
>(settings.merged.ui?.theme || DEFAULT_THEME.name);
|
||||
|
||||
// Generate theme items filtered by selected scope
|
||||
const customThemes =
|
||||
selectedScope === SettingScope.User
|
||||
? settings.user.settings.customThemes || {}
|
||||
: settings.merged.customThemes || {};
|
||||
? settings.user.settings.ui?.customThemes || {}
|
||||
: settings.merged.ui?.customThemes || {};
|
||||
const builtInThemes = themeManager
|
||||
.getAvailableThemes()
|
||||
.filter((theme) => theme.type !== 'custom');
|
||||
@@ -76,7 +76,7 @@ export function ThemeDialog({
|
||||
const [selectInputKey, setSelectInputKey] = useState(Date.now());
|
||||
|
||||
// Find the index of the selected theme, but only if it exists in the list
|
||||
const selectedThemeName = settings.merged.theme || DEFAULT_THEME.name;
|
||||
const selectedThemeName = settings.merged.ui?.theme || DEFAULT_THEME.name;
|
||||
const initialThemeIndex = themeItems.findIndex(
|
||||
(item) => item.value === selectedThemeName,
|
||||
);
|
||||
@@ -128,7 +128,7 @@ export function ThemeDialog({
|
||||
|
||||
// Generate scope message for theme setting
|
||||
const otherScopeModifiedMessage = getScopeMessageForSetting(
|
||||
'theme',
|
||||
'ui.theme',
|
||||
selectedScope,
|
||||
settings,
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const VimModeProvider = ({
|
||||
children: React.ReactNode;
|
||||
settings: LoadedSettings;
|
||||
}) => {
|
||||
const initialVimEnabled = settings.merged.vimMode ?? false;
|
||||
const initialVimEnabled = settings.merged.general?.vimMode ?? false;
|
||||
const [vimEnabled, setVimEnabled] = useState(initialVimEnabled);
|
||||
const [vimMode, setVimMode] = useState<VimMode>(
|
||||
initialVimEnabled ? 'NORMAL' : 'INSERT',
|
||||
@@ -40,13 +40,13 @@ export const VimModeProvider = ({
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize vimEnabled from settings on mount
|
||||
const enabled = settings.merged.vimMode ?? false;
|
||||
const enabled = settings.merged.general?.vimMode ?? false;
|
||||
setVimEnabled(enabled);
|
||||
// When vim mode is enabled, always start in NORMAL mode
|
||||
if (enabled) {
|
||||
setVimMode('NORMAL');
|
||||
}
|
||||
}, [settings.merged.vimMode]);
|
||||
}, [settings.merged.general?.vimMode]);
|
||||
|
||||
const toggleVimEnabled = useCallback(async () => {
|
||||
const newValue = !vimEnabled;
|
||||
@@ -55,7 +55,7 @@ export const VimModeProvider = ({
|
||||
if (newValue) {
|
||||
setVimMode('NORMAL');
|
||||
}
|
||||
await settings.setValue(SettingScope.User, 'vimMode', newValue);
|
||||
await settings.setValue(SettingScope.User, 'general.vimMode', newValue);
|
||||
return newValue;
|
||||
}, [vimEnabled, settings]);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export const useAuthCommand = (
|
||||
config: Config,
|
||||
) => {
|
||||
const [isAuthDialogOpen, setIsAuthDialogOpen] = useState(
|
||||
settings.merged.selectedAuthType === undefined,
|
||||
settings.merged.security?.auth?.selectedType === undefined,
|
||||
);
|
||||
|
||||
const openAuthDialog = useCallback(() => {
|
||||
@@ -30,7 +30,7 @@ export const useAuthCommand = (
|
||||
|
||||
useEffect(() => {
|
||||
const authFlow = async () => {
|
||||
const authType = settings.merged.selectedAuthType;
|
||||
const authType = settings.merged.security?.auth?.selectedType;
|
||||
if (isAuthDialogOpen || !authType) {
|
||||
return;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export const useAuthCommand = (
|
||||
if (authType) {
|
||||
await clearCachedCredentialFile();
|
||||
|
||||
settings.setValue(scope, 'selectedAuthType', authType);
|
||||
settings.setValue(scope, 'security.auth.selectedType', authType);
|
||||
if (
|
||||
authType === AuthType.LOGIN_WITH_GOOGLE &&
|
||||
config.isBrowserLaunchSuppressed()
|
||||
|
||||
@@ -22,7 +22,10 @@ export const useFolderTrust = (
|
||||
const [isFolderTrustDialogOpen, setIsFolderTrustDialogOpen] = useState(false);
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
|
||||
const { folderTrust, folderTrustFeature } = settings.merged;
|
||||
const folderTrust = settings.merged.security?.folderTrust?.enabled;
|
||||
const folderTrustFeature =
|
||||
settings.merged.security?.folderTrust?.featureEnabled;
|
||||
|
||||
useEffect(() => {
|
||||
const trusted = isWorkspaceTrusted({
|
||||
folderTrust,
|
||||
|
||||
@@ -32,7 +32,7 @@ export function createShowMemoryAction(
|
||||
|
||||
const currentMemory = config.getUserMemory();
|
||||
const fileCount = config.getGeminiMdFileCount();
|
||||
const contextFileName = settings.merged.contextFileName;
|
||||
const contextFileName = settings.merged.context?.fileName;
|
||||
const contextFileNames = Array.isArray(contextFileName)
|
||||
? contextFileName
|
||||
: [contextFileName];
|
||||
|
||||
@@ -29,14 +29,14 @@ export const useThemeCommand = (
|
||||
|
||||
// Check for invalid theme configuration on startup
|
||||
useEffect(() => {
|
||||
const effectiveTheme = loadedSettings.merged.theme;
|
||||
const effectiveTheme = loadedSettings.merged.ui?.theme;
|
||||
if (effectiveTheme && !themeManager.findThemeByName(effectiveTheme)) {
|
||||
setIsThemeDialogOpen(true);
|
||||
setThemeError(`Theme "${effectiveTheme}" not found.`);
|
||||
} else {
|
||||
setThemeError(null);
|
||||
}
|
||||
}, [loadedSettings.merged.theme, setThemeError]);
|
||||
}, [loadedSettings.merged.ui?.theme, setThemeError]);
|
||||
|
||||
const openThemeDialog = useCallback(() => {
|
||||
if (process.env['NO_COLOR']) {
|
||||
@@ -77,8 +77,8 @@ export const useThemeCommand = (
|
||||
try {
|
||||
// Merge user and workspace custom themes (workspace takes precedence)
|
||||
const mergedCustomThemes = {
|
||||
...(loadedSettings.user.settings.customThemes || {}),
|
||||
...(loadedSettings.workspace.settings.customThemes || {}),
|
||||
...(loadedSettings.user.settings.ui?.customThemes || {}),
|
||||
...(loadedSettings.workspace.settings.ui?.customThemes || {}),
|
||||
};
|
||||
// Only allow selecting themes available in the merged custom themes or built-in themes
|
||||
const isBuiltIn = themeManager.findThemeByName(themeName);
|
||||
@@ -88,11 +88,11 @@ export const useThemeCommand = (
|
||||
setIsThemeDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
loadedSettings.setValue(scope, 'theme', themeName); // Update the merged settings
|
||||
if (loadedSettings.merged.customThemes) {
|
||||
themeManager.loadCustomThemes(loadedSettings.merged.customThemes);
|
||||
loadedSettings.setValue(scope, 'ui.theme', themeName); // Update the merged settings
|
||||
if (loadedSettings.merged.ui?.customThemes) {
|
||||
themeManager.loadCustomThemes(loadedSettings.merged.ui?.customThemes);
|
||||
}
|
||||
applyTheme(loadedSettings.merged.theme); // Apply the current theme
|
||||
applyTheme(loadedSettings.merged.ui?.theme); // Apply the current theme
|
||||
setThemeError(null);
|
||||
} finally {
|
||||
setIsThemeDialogOpen(false); // Close the dialog
|
||||
|
||||
@@ -20,7 +20,7 @@ export function useWorkspaceMigration(settings: LoadedSettings) {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!settings.merged.extensionManagement) {
|
||||
if (!settings.merged.experimental?.extensionManagement) {
|
||||
return;
|
||||
}
|
||||
const cwd = process.cwd();
|
||||
@@ -33,7 +33,10 @@ export function useWorkspaceMigration(settings: LoadedSettings) {
|
||||
setShowWorkspaceMigrationDialog(true);
|
||||
console.log(settings.merged.extensions);
|
||||
}
|
||||
}, [settings.merged.extensions, settings.merged.extensionManagement]);
|
||||
}, [
|
||||
settings.merged.extensions,
|
||||
settings.merged.experimental?.extensionManagement,
|
||||
]);
|
||||
|
||||
const onWorkspaceMigrationDialogOpen = () => {
|
||||
const userSettings = settings.forScope(SettingScope.User);
|
||||
|
||||
@@ -134,7 +134,7 @@ export function colorizeCode(
|
||||
): React.ReactNode {
|
||||
const codeToHighlight = code.replace(/\n$/, '');
|
||||
const activeTheme = theme || themeManager.getActiveTheme();
|
||||
const showLineNumbers = settings?.merged.showLineNumbers ?? true;
|
||||
const showLineNumbers = settings?.merged.ui?.showLineNumbers ?? true;
|
||||
|
||||
try {
|
||||
// Render the HAST tree using the adapted theme
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('<MarkdownDisplay />', () => {
|
||||
{ path: '', settings: {} },
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -224,10 +225,11 @@ Another paragraph.
|
||||
const settings = new LoadedSettings(
|
||||
{ path: '', settings: {} },
|
||||
{ path: '', settings: {} },
|
||||
{ path: '', settings: { showLineNumbers: false } },
|
||||
{ path: '', settings: { ui: { showLineNumbers: false } } },
|
||||
{ path: '', settings: {} },
|
||||
[],
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { lastFrame } = render(
|
||||
|
||||
@@ -64,8 +64,10 @@ describe('handleAutoUpdate', () => {
|
||||
|
||||
mockSettings = {
|
||||
merged: {
|
||||
general: {
|
||||
disableAutoUpdate: false,
|
||||
},
|
||||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
mockChildProcess = Object.assign(new EventEmitter(), {
|
||||
@@ -93,7 +95,7 @@ describe('handleAutoUpdate', () => {
|
||||
});
|
||||
|
||||
it('should do nothing if update nag is disabled', () => {
|
||||
mockSettings.merged.disableUpdateNag = true;
|
||||
mockSettings.merged.general!.disableUpdateNag = true;
|
||||
handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);
|
||||
expect(mockGetInstallationInfo).not.toHaveBeenCalled();
|
||||
expect(mockUpdateEventEmitter.emit).not.toHaveBeenCalled();
|
||||
@@ -101,7 +103,7 @@ describe('handleAutoUpdate', () => {
|
||||
});
|
||||
|
||||
it('should emit "update-received" but not update if auto-updates are disabled', () => {
|
||||
mockSettings.merged.disableAutoUpdate = true;
|
||||
mockSettings.merged.general!.disableAutoUpdate = true;
|
||||
mockGetInstallationInfo.mockReturnValue({
|
||||
updateCommand: 'npm i -g @google/gemini-cli@latest',
|
||||
updateMessage: 'Please update manually.',
|
||||
|
||||
@@ -23,13 +23,13 @@ export function handleAutoUpdate(
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.merged.disableUpdateNag) {
|
||||
if (settings.merged.general?.disableUpdateNag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installationInfo = getInstallationInfo(
|
||||
projectRoot,
|
||||
settings.merged.disableAutoUpdate ?? false,
|
||||
settings.merged.general?.disableAutoUpdate ?? false,
|
||||
);
|
||||
|
||||
let combinedMessage = info.message;
|
||||
@@ -41,7 +41,10 @@ export function handleAutoUpdate(
|
||||
message: combinedMessage,
|
||||
});
|
||||
|
||||
if (!installationInfo.updateCommand || settings.merged.disableAutoUpdate) {
|
||||
if (
|
||||
!installationInfo.updateCommand ||
|
||||
settings.merged.general?.disableAutoUpdate
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const isNightly = info.update.latest.includes('nightly');
|
||||
|
||||
@@ -42,12 +42,7 @@ describe('SettingsUtils', () => {
|
||||
const categories = getSettingsByCategory();
|
||||
|
||||
expect(categories).toHaveProperty('General');
|
||||
expect(categories).toHaveProperty('Accessibility');
|
||||
expect(categories).toHaveProperty('Checkpointing');
|
||||
expect(categories).toHaveProperty('File Filtering');
|
||||
expect(categories).toHaveProperty('UI');
|
||||
expect(categories).toHaveProperty('Mode');
|
||||
expect(categories).toHaveProperty('Updates');
|
||||
});
|
||||
|
||||
it('should include key property in grouped settings', () => {
|
||||
@@ -63,7 +58,7 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getSettingDefinition', () => {
|
||||
it('should return definition for valid setting', () => {
|
||||
const definition = getSettingDefinition('showMemoryUsage');
|
||||
const definition = getSettingDefinition('ui.showMemoryUsage');
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition?.label).toBe('Show Memory Usage');
|
||||
});
|
||||
@@ -76,13 +71,13 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('requiresRestart', () => {
|
||||
it('should return true for settings that require restart', () => {
|
||||
expect(requiresRestart('autoConfigureMaxOldSpaceSize')).toBe(true);
|
||||
expect(requiresRestart('checkpointing.enabled')).toBe(true);
|
||||
expect(requiresRestart('advanced.autoConfigureMemory')).toBe(true);
|
||||
expect(requiresRestart('general.checkpointing.enabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for settings that do not require restart', () => {
|
||||
expect(requiresRestart('showMemoryUsage')).toBe(false);
|
||||
expect(requiresRestart('hideTips')).toBe(false);
|
||||
expect(requiresRestart('ui.showMemoryUsage')).toBe(false);
|
||||
expect(requiresRestart('ui.hideTips')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for invalid settings', () => {
|
||||
@@ -92,10 +87,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getDefaultValue', () => {
|
||||
it('should return correct default values', () => {
|
||||
expect(getDefaultValue('showMemoryUsage')).toBe(false);
|
||||
expect(getDefaultValue('fileFiltering.enableRecursiveFileSearch')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(getDefaultValue('ui.showMemoryUsage')).toBe(false);
|
||||
expect(
|
||||
getDefaultValue('context.fileFiltering.enableRecursiveFileSearch'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return undefined for invalid settings', () => {
|
||||
@@ -106,19 +101,19 @@ describe('SettingsUtils', () => {
|
||||
describe('getRestartRequiredSettings', () => {
|
||||
it('should return all settings that require restart', () => {
|
||||
const restartSettings = getRestartRequiredSettings();
|
||||
expect(restartSettings).toContain('autoConfigureMaxOldSpaceSize');
|
||||
expect(restartSettings).toContain('checkpointing.enabled');
|
||||
expect(restartSettings).not.toContain('showMemoryUsage');
|
||||
expect(restartSettings).toContain('advanced.autoConfigureMemory');
|
||||
expect(restartSettings).toContain('general.checkpointing.enabled');
|
||||
expect(restartSettings).not.toContain('ui.showMemoryUsage');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEffectiveValue', () => {
|
||||
it('should return value from settings when set', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -127,10 +122,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return value from merged settings when not set in current scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -142,7 +137,7 @@ describe('SettingsUtils', () => {
|
||||
const mergedSettings = {};
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -151,14 +146,14 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should handle nested settings correctly', () => {
|
||||
const settings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
const mergedSettings = {
|
||||
accessibility: { disableLoadingPhrases: false },
|
||||
ui: { accessibility: { disableLoadingPhrases: false } },
|
||||
};
|
||||
|
||||
const value = getEffectiveValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -181,9 +176,9 @@ describe('SettingsUtils', () => {
|
||||
describe('getAllSettingKeys', () => {
|
||||
it('should return all setting keys', () => {
|
||||
const keys = getAllSettingKeys();
|
||||
expect(keys).toContain('showMemoryUsage');
|
||||
expect(keys).toContain('accessibility.disableLoadingPhrases');
|
||||
expect(keys).toContain('checkpointing.enabled');
|
||||
expect(keys).toContain('ui.showMemoryUsage');
|
||||
expect(keys).toContain('ui.accessibility.disableLoadingPhrases');
|
||||
expect(keys).toContain('general.checkpointing.enabled');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,10 +204,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('isValidSettingKey', () => {
|
||||
it('should return true for valid setting keys', () => {
|
||||
expect(isValidSettingKey('showMemoryUsage')).toBe(true);
|
||||
expect(isValidSettingKey('accessibility.disableLoadingPhrases')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isValidSettingKey('ui.showMemoryUsage')).toBe(true);
|
||||
expect(
|
||||
isValidSettingKey('ui.accessibility.disableLoadingPhrases'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid setting keys', () => {
|
||||
@@ -223,10 +218,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getSettingCategory', () => {
|
||||
it('should return correct category for valid settings', () => {
|
||||
expect(getSettingCategory('showMemoryUsage')).toBe('UI');
|
||||
expect(getSettingCategory('accessibility.disableLoadingPhrases')).toBe(
|
||||
'Accessibility',
|
||||
);
|
||||
expect(getSettingCategory('ui.showMemoryUsage')).toBe('UI');
|
||||
expect(
|
||||
getSettingCategory('ui.accessibility.disableLoadingPhrases'),
|
||||
).toBe('UI');
|
||||
});
|
||||
|
||||
it('should return undefined for invalid settings', () => {
|
||||
@@ -236,18 +231,20 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('shouldShowInDialog', () => {
|
||||
it('should return true for settings marked to show in dialog', () => {
|
||||
expect(shouldShowInDialog('showMemoryUsage')).toBe(true);
|
||||
expect(shouldShowInDialog('vimMode')).toBe(true);
|
||||
expect(shouldShowInDialog('hideWindowTitle')).toBe(true);
|
||||
expect(shouldShowInDialog('usageStatisticsEnabled')).toBe(false);
|
||||
expect(shouldShowInDialog('ui.showMemoryUsage')).toBe(true);
|
||||
expect(shouldShowInDialog('general.vimMode')).toBe(true);
|
||||
expect(shouldShowInDialog('ui.hideWindowTitle')).toBe(true);
|
||||
expect(shouldShowInDialog('privacy.usageStatisticsEnabled')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for settings marked to hide from dialog', () => {
|
||||
expect(shouldShowInDialog('selectedAuthType')).toBe(false);
|
||||
expect(shouldShowInDialog('coreTools')).toBe(false);
|
||||
expect(shouldShowInDialog('customThemes')).toBe(false);
|
||||
expect(shouldShowInDialog('theme')).toBe(false); // Changed to false
|
||||
expect(shouldShowInDialog('preferredEditor')).toBe(false); // Changed to false
|
||||
expect(shouldShowInDialog('security.auth.selectedType')).toBe(false);
|
||||
expect(shouldShowInDialog('tools.core')).toBe(false);
|
||||
expect(shouldShowInDialog('ui.customThemes')).toBe(false);
|
||||
expect(shouldShowInDialog('ui.theme')).toBe(false); // Changed to false
|
||||
expect(shouldShowInDialog('general.preferredEditor')).toBe(false); // Changed to false
|
||||
});
|
||||
|
||||
it('should return true for invalid settings (default behavior)', () => {
|
||||
@@ -263,10 +260,10 @@ describe('SettingsUtils', () => {
|
||||
expect(categories['UI']).toBeDefined();
|
||||
const uiSettings = categories['UI'];
|
||||
const uiKeys = uiSettings.map((s) => s.key);
|
||||
expect(uiKeys).toContain('showMemoryUsage');
|
||||
expect(uiKeys).toContain('hideWindowTitle');
|
||||
expect(uiKeys).not.toContain('customThemes'); // This is marked false
|
||||
expect(uiKeys).not.toContain('theme'); // This is now marked false
|
||||
expect(uiKeys).toContain('ui.showMemoryUsage');
|
||||
expect(uiKeys).toContain('ui.hideWindowTitle');
|
||||
expect(uiKeys).not.toContain('ui.customThemes'); // This is marked false
|
||||
expect(uiKeys).not.toContain('ui.theme'); // This is now marked false
|
||||
});
|
||||
|
||||
it('should not include Advanced category settings', () => {
|
||||
@@ -282,15 +279,15 @@ describe('SettingsUtils', () => {
|
||||
const allSettings = Object.values(categories).flat();
|
||||
const allKeys = allSettings.map((s) => s.key);
|
||||
|
||||
expect(allKeys).toContain('vimMode');
|
||||
expect(allKeys).toContain('ideMode');
|
||||
expect(allKeys).toContain('disableAutoUpdate');
|
||||
expect(allKeys).toContain('showMemoryUsage');
|
||||
expect(allKeys).not.toContain('usageStatisticsEnabled');
|
||||
expect(allKeys).not.toContain('selectedAuthType');
|
||||
expect(allKeys).not.toContain('coreTools');
|
||||
expect(allKeys).not.toContain('theme'); // Now hidden
|
||||
expect(allKeys).not.toContain('preferredEditor'); // Now hidden
|
||||
expect(allKeys).toContain('general.vimMode');
|
||||
expect(allKeys).toContain('ide.enabled');
|
||||
expect(allKeys).toContain('general.disableAutoUpdate');
|
||||
expect(allKeys).toContain('ui.showMemoryUsage');
|
||||
expect(allKeys).not.toContain('privacy.usageStatisticsEnabled');
|
||||
expect(allKeys).not.toContain('security.auth.selectedType');
|
||||
expect(allKeys).not.toContain('tools.core');
|
||||
expect(allKeys).not.toContain('ui.theme'); // Now hidden
|
||||
expect(allKeys).not.toContain('general.preferredEditor'); // Now hidden
|
||||
});
|
||||
});
|
||||
|
||||
@@ -299,12 +296,12 @@ describe('SettingsUtils', () => {
|
||||
const booleanSettings = getDialogSettingsByType('boolean');
|
||||
|
||||
const keys = booleanSettings.map((s) => s.key);
|
||||
expect(keys).toContain('showMemoryUsage');
|
||||
expect(keys).toContain('vimMode');
|
||||
expect(keys).toContain('hideWindowTitle');
|
||||
expect(keys).not.toContain('usageStatisticsEnabled');
|
||||
expect(keys).not.toContain('selectedAuthType'); // Advanced setting
|
||||
expect(keys).not.toContain('useExternalAuth'); // Advanced setting
|
||||
expect(keys).toContain('ui.showMemoryUsage');
|
||||
expect(keys).toContain('general.vimMode');
|
||||
expect(keys).toContain('ui.hideWindowTitle');
|
||||
expect(keys).not.toContain('privacy.usageStatisticsEnabled');
|
||||
expect(keys).not.toContain('security.auth.selectedType'); // Advanced setting
|
||||
expect(keys).not.toContain('security.auth.useExternal'); // Advanced setting
|
||||
});
|
||||
|
||||
it('should return only string dialog settings', () => {
|
||||
@@ -312,9 +309,9 @@ describe('SettingsUtils', () => {
|
||||
|
||||
const keys = stringSettings.map((s) => s.key);
|
||||
// Note: theme and preferredEditor are now hidden from dialog
|
||||
expect(keys).not.toContain('theme'); // Now marked false
|
||||
expect(keys).not.toContain('preferredEditor'); // Now marked false
|
||||
expect(keys).not.toContain('selectedAuthType'); // Advanced setting
|
||||
expect(keys).not.toContain('ui.theme'); // Now marked false
|
||||
expect(keys).not.toContain('general.preferredEditor'); // Now marked false
|
||||
expect(keys).not.toContain('security.auth.selectedType'); // Advanced setting
|
||||
|
||||
// Most string settings are now hidden, so let's just check they exclude advanced ones
|
||||
expect(keys.every((key) => !key.startsWith('tool'))).toBe(true); // No tool-related settings
|
||||
@@ -326,24 +323,28 @@ describe('SettingsUtils', () => {
|
||||
const dialogKeys = getDialogSettingKeys();
|
||||
|
||||
// Should include settings marked for dialog
|
||||
expect(dialogKeys).toContain('showMemoryUsage');
|
||||
expect(dialogKeys).toContain('vimMode');
|
||||
expect(dialogKeys).toContain('hideWindowTitle');
|
||||
expect(dialogKeys).not.toContain('usageStatisticsEnabled');
|
||||
expect(dialogKeys).toContain('ideMode');
|
||||
expect(dialogKeys).toContain('disableAutoUpdate');
|
||||
expect(dialogKeys).toContain('ui.showMemoryUsage');
|
||||
expect(dialogKeys).toContain('general.vimMode');
|
||||
expect(dialogKeys).toContain('ui.hideWindowTitle');
|
||||
expect(dialogKeys).not.toContain('privacy.usageStatisticsEnabled');
|
||||
expect(dialogKeys).toContain('ide.enabled');
|
||||
expect(dialogKeys).toContain('general.disableAutoUpdate');
|
||||
|
||||
// Should include nested settings marked for dialog
|
||||
expect(dialogKeys).toContain('fileFiltering.respectGitIgnore');
|
||||
expect(dialogKeys).toContain('fileFiltering.respectGeminiIgnore');
|
||||
expect(dialogKeys).toContain('fileFiltering.enableRecursiveFileSearch');
|
||||
expect(dialogKeys).toContain('context.fileFiltering.respectGitIgnore');
|
||||
expect(dialogKeys).toContain(
|
||||
'context.fileFiltering.respectGeminiIgnore',
|
||||
);
|
||||
expect(dialogKeys).toContain(
|
||||
'context.fileFiltering.enableRecursiveFileSearch',
|
||||
);
|
||||
|
||||
// Should NOT include settings marked as hidden
|
||||
expect(dialogKeys).not.toContain('theme'); // Hidden
|
||||
expect(dialogKeys).not.toContain('customThemes'); // Hidden
|
||||
expect(dialogKeys).not.toContain('preferredEditor'); // Hidden
|
||||
expect(dialogKeys).not.toContain('selectedAuthType'); // Advanced
|
||||
expect(dialogKeys).not.toContain('coreTools'); // Advanced
|
||||
expect(dialogKeys).not.toContain('ui.theme'); // Hidden
|
||||
expect(dialogKeys).not.toContain('ui.customThemes'); // Hidden
|
||||
expect(dialogKeys).not.toContain('general.preferredEditor'); // Hidden
|
||||
expect(dialogKeys).not.toContain('security.auth.selectedType'); // Advanced
|
||||
expect(dialogKeys).not.toContain('tools.core'); // Advanced
|
||||
expect(dialogKeys).not.toContain('mcpServers'); // Advanced
|
||||
expect(dialogKeys).not.toContain('telemetry'); // Advanced
|
||||
});
|
||||
@@ -358,7 +359,7 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should handle nested settings display correctly', () => {
|
||||
// Test the specific issue with fileFiltering.respectGitIgnore
|
||||
const key = 'fileFiltering.respectGitIgnore';
|
||||
const key = 'context.fileFiltering.respectGitIgnore';
|
||||
const initialSettings = {};
|
||||
const pendingSettings = {};
|
||||
|
||||
@@ -411,11 +412,11 @@ describe('SettingsUtils', () => {
|
||||
describe('Business Logic Utilities', () => {
|
||||
describe('getSettingValue', () => {
|
||||
it('should return value from settings when set', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
|
||||
const value = getSettingValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -424,10 +425,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return value from merged settings when not set in current scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const value = getSettingValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -449,51 +450,68 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('isSettingModified', () => {
|
||||
it('should return true when value differs from default', () => {
|
||||
expect(isSettingModified('showMemoryUsage', true)).toBe(true);
|
||||
expect(isSettingModified('ui.showMemoryUsage', true)).toBe(true);
|
||||
expect(
|
||||
isSettingModified('fileFiltering.enableRecursiveFileSearch', false),
|
||||
isSettingModified(
|
||||
'context.fileFiltering.enableRecursiveFileSearch',
|
||||
false,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when value matches default', () => {
|
||||
expect(isSettingModified('showMemoryUsage', false)).toBe(false);
|
||||
expect(isSettingModified('ui.showMemoryUsage', false)).toBe(false);
|
||||
expect(
|
||||
isSettingModified('fileFiltering.enableRecursiveFileSearch', true),
|
||||
isSettingModified(
|
||||
'context.fileFiltering.enableRecursiveFileSearch',
|
||||
true,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('settingExistsInScope', () => {
|
||||
it('should return true for top-level settings that exist', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
expect(settingExistsInScope('showMemoryUsage', settings)).toBe(true);
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
expect(settingExistsInScope('ui.showMemoryUsage', settings)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for top-level settings that do not exist', () => {
|
||||
const settings = {};
|
||||
expect(settingExistsInScope('showMemoryUsage', settings)).toBe(false);
|
||||
expect(settingExistsInScope('ui.showMemoryUsage', settings)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true for nested settings that exist', () => {
|
||||
const settings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
expect(
|
||||
settingExistsInScope('accessibility.disableLoadingPhrases', settings),
|
||||
settingExistsInScope(
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for nested settings that do not exist', () => {
|
||||
const settings = {};
|
||||
expect(
|
||||
settingExistsInScope('accessibility.disableLoadingPhrases', settings),
|
||||
settingExistsInScope(
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when parent exists but child does not', () => {
|
||||
const settings = { accessibility: {} };
|
||||
const settings = { ui: { accessibility: {} } };
|
||||
expect(
|
||||
settingExistsInScope('accessibility.disableLoadingPhrases', settings),
|
||||
settingExistsInScope(
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -502,41 +520,41 @@ describe('SettingsUtils', () => {
|
||||
it('should set top-level setting value', () => {
|
||||
const pendingSettings = {};
|
||||
const result = setPendingSettingValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
expect(result.showMemoryUsage).toBe(true);
|
||||
expect(result.ui?.showMemoryUsage).toBe(true);
|
||||
});
|
||||
|
||||
it('should set nested setting value', () => {
|
||||
const pendingSettings = {};
|
||||
const result = setPendingSettingValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
expect(result.accessibility?.disableLoadingPhrases).toBe(true);
|
||||
expect(result.ui?.accessibility?.disableLoadingPhrases).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve existing nested settings', () => {
|
||||
const pendingSettings = {
|
||||
accessibility: { disableLoadingPhrases: false },
|
||||
ui: { accessibility: { disableLoadingPhrases: false } },
|
||||
};
|
||||
const result = setPendingSettingValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
true,
|
||||
pendingSettings,
|
||||
);
|
||||
|
||||
expect(result.accessibility?.disableLoadingPhrases).toBe(true);
|
||||
expect(result.ui?.accessibility?.disableLoadingPhrases).toBe(true);
|
||||
});
|
||||
|
||||
it('should not mutate original settings', () => {
|
||||
const pendingSettings = {};
|
||||
setPendingSettingValue('showMemoryUsage', true, pendingSettings);
|
||||
setPendingSettingValue('ui.showMemoryUsage', true, pendingSettings);
|
||||
|
||||
expect(pendingSettings).toEqual({});
|
||||
});
|
||||
@@ -545,16 +563,16 @@ describe('SettingsUtils', () => {
|
||||
describe('hasRestartRequiredSettings', () => {
|
||||
it('should return true when modified settings require restart', () => {
|
||||
const modifiedSettings = new Set<string>([
|
||||
'autoConfigureMaxOldSpaceSize',
|
||||
'showMemoryUsage',
|
||||
'advanced.autoConfigureMemory',
|
||||
'ui.showMemoryUsage',
|
||||
]);
|
||||
expect(hasRestartRequiredSettings(modifiedSettings)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no modified settings require restart', () => {
|
||||
const modifiedSettings = new Set<string>([
|
||||
'showMemoryUsage',
|
||||
'hideTips',
|
||||
'ui.showMemoryUsage',
|
||||
'ui.hideTips',
|
||||
]);
|
||||
expect(hasRestartRequiredSettings(modifiedSettings)).toBe(false);
|
||||
});
|
||||
@@ -568,15 +586,15 @@ describe('SettingsUtils', () => {
|
||||
describe('getRestartRequiredFromModified', () => {
|
||||
it('should return only settings that require restart', () => {
|
||||
const modifiedSettings = new Set<string>([
|
||||
'autoConfigureMaxOldSpaceSize',
|
||||
'showMemoryUsage',
|
||||
'checkpointing.enabled',
|
||||
'advanced.autoConfigureMemory',
|
||||
'ui.showMemoryUsage',
|
||||
'general.checkpointing.enabled',
|
||||
]);
|
||||
const result = getRestartRequiredFromModified(modifiedSettings);
|
||||
|
||||
expect(result).toContain('autoConfigureMaxOldSpaceSize');
|
||||
expect(result).toContain('checkpointing.enabled');
|
||||
expect(result).not.toContain('showMemoryUsage');
|
||||
expect(result).toContain('advanced.autoConfigureMemory');
|
||||
expect(result).toContain('general.checkpointing.enabled');
|
||||
expect(result).not.toContain('ui.showMemoryUsage');
|
||||
});
|
||||
|
||||
it('should return empty array when no settings require restart', () => {
|
||||
@@ -592,12 +610,12 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getDisplayValue', () => {
|
||||
it('should show value without * when setting matches default', () => {
|
||||
const settings = { showMemoryUsage: false }; // false matches default, so no *
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const settings = { ui: { showMemoryUsage: false } }; // false matches default, so no *
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -607,11 +625,11 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should show default value when setting is not in scope', () => {
|
||||
const settings = {}; // no setting in scope
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -620,12 +638,12 @@ describe('SettingsUtils', () => {
|
||||
});
|
||||
|
||||
it('should show value with * when changed from default', () => {
|
||||
const settings = { showMemoryUsage: true }; // true is different from default (false)
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const settings = { ui: { showMemoryUsage: true } }; // true is different from default (false)
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -635,11 +653,11 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should show default value without * when setting does not exist in scope', () => {
|
||||
const settings = {}; // setting doesn't exist in scope, show default
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
const modifiedSettings = new Set<string>();
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -649,12 +667,12 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should show value with * when user changes from default', () => {
|
||||
const settings = {}; // setting doesn't exist in scope originally
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const modifiedSettings = new Set<string>(['showMemoryUsage']);
|
||||
const pendingSettings = { showMemoryUsage: true }; // user changed to true
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
const modifiedSettings = new Set<string>(['ui.showMemoryUsage']);
|
||||
const pendingSettings = { ui: { showMemoryUsage: true } }; // user changed to true
|
||||
|
||||
const result = getDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
@@ -668,14 +686,14 @@ describe('SettingsUtils', () => {
|
||||
it('should return true when setting does not exist in scope', () => {
|
||||
const settings = {}; // setting doesn't exist
|
||||
|
||||
const result = isDefaultValue('showMemoryUsage', settings);
|
||||
const result = isDefaultValue('ui.showMemoryUsage', settings);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when setting exists in scope', () => {
|
||||
const settings = { showMemoryUsage: true }; // setting exists
|
||||
const settings = { ui: { showMemoryUsage: true } }; // setting exists
|
||||
|
||||
const result = isDefaultValue('showMemoryUsage', settings);
|
||||
const result = isDefaultValue('ui.showMemoryUsage', settings);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
@@ -683,17 +701,19 @@ describe('SettingsUtils', () => {
|
||||
const settings = {}; // nested setting doesn't exist
|
||||
|
||||
const result = isDefaultValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when nested setting exists in scope', () => {
|
||||
const settings = { accessibility: { disableLoadingPhrases: true } }; // nested setting exists
|
||||
const settings = {
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
}; // nested setting exists
|
||||
|
||||
const result = isDefaultValue(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
@@ -702,11 +722,11 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('isValueInherited', () => {
|
||||
it('should return false for top-level settings that exist in scope', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const result = isValueInherited(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -715,10 +735,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return true for top-level settings that do not exist in scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const result = isValueInherited(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -727,14 +747,14 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return false for nested settings that exist in scope', () => {
|
||||
const settings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
const mergedSettings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
|
||||
const result = isValueInherited(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -744,11 +764,11 @@ describe('SettingsUtils', () => {
|
||||
it('should return true for nested settings that do not exist in scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = {
|
||||
accessibility: { disableLoadingPhrases: true },
|
||||
ui: { accessibility: { disableLoadingPhrases: true } },
|
||||
};
|
||||
|
||||
const result = isValueInherited(
|
||||
'accessibility.disableLoadingPhrases',
|
||||
'ui.accessibility.disableLoadingPhrases',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -758,11 +778,11 @@ describe('SettingsUtils', () => {
|
||||
|
||||
describe('getEffectiveDisplayValue', () => {
|
||||
it('should return value from settings when available', () => {
|
||||
const settings = { showMemoryUsage: true };
|
||||
const mergedSettings = { showMemoryUsage: false };
|
||||
const settings = { ui: { showMemoryUsage: true } };
|
||||
const mergedSettings = { ui: { showMemoryUsage: false } };
|
||||
|
||||
const result = getEffectiveDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -771,10 +791,10 @@ describe('SettingsUtils', () => {
|
||||
|
||||
it('should return value from merged settings when not in scope', () => {
|
||||
const settings = {};
|
||||
const mergedSettings = { showMemoryUsage: true };
|
||||
const mergedSettings = { ui: { showMemoryUsage: true } };
|
||||
|
||||
const result = getEffectiveDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
@@ -786,7 +806,7 @@ describe('SettingsUtils', () => {
|
||||
const mergedSettings = {};
|
||||
|
||||
const result = getEffectiveDisplayValue(
|
||||
'showMemoryUsage',
|
||||
'ui.showMemoryUsage',
|
||||
settings,
|
||||
mergedSettings,
|
||||
);
|
||||
|
||||
@@ -399,22 +399,7 @@ export function saveModifiedSettings(
|
||||
const isDefaultValue = value === getDefaultValue(settingKey);
|
||||
|
||||
if (existsInOriginalFile || !isDefaultValue) {
|
||||
// This is tricky because setValue only works on top-level keys.
|
||||
// We need to set the whole parent object.
|
||||
const [parentKey] = path;
|
||||
if (parentKey) {
|
||||
const newParentValue = setPendingSettingValueAny(
|
||||
settingKey,
|
||||
value,
|
||||
loadedSettings.forScope(scope).settings,
|
||||
)[parentKey as keyof Settings];
|
||||
|
||||
loadedSettings.setValue(
|
||||
scope,
|
||||
parentKey as keyof Settings,
|
||||
newParentValue,
|
||||
);
|
||||
}
|
||||
loadedSettings.setValue(scope, settingKey, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,7 +117,11 @@ class GeminiAgent {
|
||||
|
||||
await clearCachedCredentialFile();
|
||||
await this.config.refreshAuth(method);
|
||||
this.settings.setValue(SettingScope.User, 'selectedAuthType', method);
|
||||
this.settings.setValue(
|
||||
SettingScope.User,
|
||||
'security.auth.selectedType',
|
||||
method,
|
||||
);
|
||||
}
|
||||
|
||||
async newSession({
|
||||
@@ -128,9 +132,11 @@ class GeminiAgent {
|
||||
const config = await this.newSessionConfig(sessionId, cwd, mcpServers);
|
||||
|
||||
let isAuthenticated = false;
|
||||
if (this.settings.merged.selectedAuthType) {
|
||||
if (this.settings.merged.security?.auth?.selectedType) {
|
||||
try {
|
||||
await config.refreshAuth(this.settings.merged.selectedAuthType);
|
||||
await config.refreshAuth(
|
||||
this.settings.merged.security.auth.selectedType,
|
||||
);
|
||||
isAuthenticated = true;
|
||||
} catch (e) {
|
||||
console.error(`Authentication failed: ${e}`);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
export * from './src/index.js';
|
||||
export { Storage } from './src/config/storage.js';
|
||||
export {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
|
||||
Reference in New Issue
Block a user