refactor: refactor settings to a nested structure (#7244)

This commit is contained in:
Gal Zahavi
2025-08-27 18:39:45 -07:00
committed by GitHub
parent b8a7bfd136
commit f22263c9e8
41 changed files with 2852 additions and 1424 deletions

25
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -333,7 +333,7 @@ describe('loadCliConfig', () => {
it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { showMemoryUsage: false };
const settings: Settings = { ui: { showMemoryUsage: false } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getShowMemoryUsage()).toBe(false);
});
@@ -341,7 +341,7 @@ describe('loadCliConfig', () => {
it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => {
process.argv = ['node', 'script.js', '--show-memory-usage'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { showMemoryUsage: false };
const settings: Settings = { ui: { showMemoryUsage: false } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getShowMemoryUsage()).toBe(true);
});
@@ -804,7 +804,7 @@ describe('mergeExcludeTools', () => {
});
it('should merge excludeTools from settings and extensions', async () => {
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
const extensions: Extension[] = [
{
path: '/path/to/ext1',
@@ -840,7 +840,7 @@ describe('mergeExcludeTools', () => {
});
it('should handle overlapping excludeTools between settings and extensions', async () => {
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
const extensions: Extension[] = [
{
path: '/path/to/ext1',
@@ -867,7 +867,7 @@ describe('mergeExcludeTools', () => {
});
it('should handle overlapping excludeTools between extensions', async () => {
const settings: Settings = { excludeTools: ['tool1'] };
const settings: Settings = { tools: { exclude: ['tool1'] } };
const extensions: Extension[] = [
{
path: '/path/to/ext1',
@@ -935,7 +935,7 @@ describe('mergeExcludeTools', () => {
it('should handle settings with excludeTools but no extensions', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { excludeTools: ['tool1', 'tool2'] };
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
const extensions: Extension[] = [];
const config = await loadCliConfig(
settings,
@@ -977,7 +977,7 @@ describe('mergeExcludeTools', () => {
});
it('should not modify the original settings object', async () => {
const settings: Settings = { excludeTools: ['tool1'] };
const settings: Settings = { tools: { exclude: ['tool1'] } };
const extensions: Extension[] = [
{
path: '/path/to/ext',
@@ -1166,7 +1166,7 @@ describe('Approval mode tool exclusion logic', () => {
'test',
];
const argv = await parseArguments({} as Settings);
const settings: Settings = { excludeTools: ['custom_tool'] };
const settings: Settings = { tools: { exclude: ['custom_tool'] } };
const extensions: Extension[] = [];
const config = await loadCliConfig(
@@ -1297,7 +1297,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
allowMCPServers: ['server1', 'server2'],
mcp: { allowed: ['server1', 'server2'] },
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
@@ -1311,7 +1311,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
excludeMCPServers: ['server1', 'server2'],
mcp: { excluded: ['server1', 'server2'] },
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
@@ -1324,8 +1324,10 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
excludeMCPServers: ['server1'],
allowMCPServers: ['server1', 'server2'],
mcp: {
excluded: ['server1'],
allowed: ['server1', 'server2'],
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
@@ -1343,14 +1345,40 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
excludeMCPServers: ['server1'],
allowMCPServers: ['server2'],
mcp: {
excluded: ['server1'],
allowed: ['server2'],
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server1: { url: 'http://localhost:8080' },
});
});
it('should prioritize CLI flag over both allowed and excluded settings', async () => {
process.argv = [
'node',
'script.js',
'--allowed-mcp-server-names',
'server2',
'--allowed-mcp-server-names',
'server3',
];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
...baseSettings,
mcp: {
allowed: ['server1', 'server2'], // Should be ignored
excluded: ['server3'], // Should be ignored
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getMcpServers()).toEqual({
server2: { url: 'http://localhost:8081' },
server3: { url: 'http://localhost:8082' },
});
});
});
describe('loadCliConfig extensions', () => {
@@ -1403,7 +1431,9 @@ describe('loadCliConfig model selection', () => {
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(
{
model: 'gemini-9001-ultra',
model: {
name: 'gemini-9001-ultra',
},
},
[],
'test-session',
@@ -1433,7 +1463,9 @@ describe('loadCliConfig model selection', () => {
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(
{
model: 'gemini-9001-ultra',
model: {
name: 'gemini-9001-ultra',
},
},
[],
'test-session',
@@ -1485,7 +1517,9 @@ describe('loadCliConfig folderTrustFeature', () => {
it('should be true when settings.folderTrustFeature is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature: true };
const settings: Settings = {
security: { folderTrust: { featureEnabled: true } },
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrustFeature()).toBe(true);
});
@@ -1509,8 +1543,12 @@ describe('loadCliConfig folderTrust', () => {
it('should be false if folderTrustFeature is false and folderTrust is false', async () => {
process.argv = ['node', 'script.js'];
const settings: Settings = {
folderTrustFeature: false,
folderTrust: false,
security: {
folderTrust: {
featureEnabled: false,
enabled: false,
},
},
};
const argv = await parseArguments({} as Settings);
const config = await loadCliConfig(settings, [], 'test-session', argv);
@@ -1520,7 +1558,14 @@ describe('loadCliConfig folderTrust', () => {
it('should be false if folderTrustFeature is true and folderTrust is false', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature: true, folderTrust: false };
const settings: Settings = {
security: {
folderTrust: {
featureEnabled: true,
enabled: false,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrust()).toBe(false);
});
@@ -1528,7 +1573,14 @@ describe('loadCliConfig folderTrust', () => {
it('should be false if folderTrustFeature is false and folderTrust is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature: false, folderTrust: true };
const settings: Settings = {
security: {
folderTrust: {
featureEnabled: false,
enabled: true,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrust()).toBe(false);
});
@@ -1536,7 +1588,14 @@ describe('loadCliConfig folderTrust', () => {
it('should be true when folderTrustFeature is true and folderTrust is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature: true, folderTrust: true };
const settings: Settings = {
security: {
folderTrust: {
featureEnabled: true,
enabled: true,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrust()).toBe(true);
});
@@ -1570,11 +1629,13 @@ describe('loadCliConfig with includeDirectories', () => {
];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
includeDirectories: [
path.resolve(path.sep, 'settings', 'path1'),
path.join(os.homedir(), 'settings', 'path2'),
path.join(mockCwd, 'settings', 'path3'),
],
context: {
includeDirectories: [
path.resolve(path.sep, 'settings', 'path1'),
path.join(os.homedir(), 'settings', 'path2'),
path.join(mockCwd, 'settings', 'path3'),
],
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
const expected = [
@@ -1613,8 +1674,10 @@ describe('loadCliConfig chatCompression', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
chatCompression: {
contextPercentageThreshold: 0.5,
model: {
chatCompression: {
contextPercentageThreshold: 0.5,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
@@ -1658,7 +1721,7 @@ describe('loadCliConfig useRipgrep', () => {
it('should be true when useRipgrep is set to true in settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { useRipgrep: true };
const settings: Settings = { tools: { useRipgrep: true } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getUseRipgrep()).toBe(true);
});
@@ -1666,7 +1729,7 @@ describe('loadCliConfig useRipgrep', () => {
it('should be false when useRipgrep is explicitly set to false in settings', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { useRipgrep: false };
const settings: Settings = { tools: { useRipgrep: false } };
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getUseRipgrep()).toBe(false);
});
@@ -2002,13 +2065,26 @@ describe('loadCliConfig trustedFolder', () => {
} of testCases) {
it(`should be correct for: ${description}`, async () => {
(isWorkspaceTrusted as Mock).mockImplementation((settings: Settings) => {
const featureIsEnabled =
(settings.folderTrustFeature ?? false) &&
(settings.folderTrust ?? true);
return featureIsEnabled ? mockTrustValue : true;
const folderTrustFeature =
settings.security?.folderTrust?.featureEnabled ?? false;
const folderTrustSetting =
settings.security?.folderTrust?.enabled ?? true;
const folderTrustEnabled = folderTrustFeature && folderTrustSetting;
if (!folderTrustEnabled) {
return true;
}
return mockTrustValue; // This is the part that comes from the test case
});
const argv = await parseArguments({} as Settings);
const settings: Settings = { folderTrustFeature, folderTrust };
const settings: Settings = {
security: {
folderTrust: {
featureEnabled: folderTrustFeature,
enabled: folderTrust,
},
},
};
const config = await loadCliConfig(settings, [], 'test-session', argv);
expect(config.getFolderTrust()).toBe(expectedFolderTrust);

View File

@@ -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) {

View File

@@ -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));
}

View File

@@ -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

View File

@@ -18,6 +18,7 @@ import { DefaultLight } from '../ui/themes/default-light.js';
import { DefaultDark } from '../ui/themes/default.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import type { Settings, MemoryImportFormat } from './settingsSchema.js';
import { mergeWith } from 'lodash-es';
export type { Settings, MemoryImportFormat };
@@ -27,6 +28,58 @@ export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
const MIGRATE_V2_OVERWRITE = false;
// As defined in spec.md
const MIGRATION_MAP: Record<string, string> = {
preferredEditor: 'general.preferredEditor',
vimMode: 'general.vimMode',
disableAutoUpdate: 'general.disableAutoUpdate',
disableUpdateNag: 'general.disableUpdateNag',
checkpointing: 'general.checkpointing',
theme: 'ui.theme',
customThemes: 'ui.customThemes',
hideWindowTitle: 'ui.hideWindowTitle',
hideTips: 'ui.hideTips',
hideBanner: 'ui.hideBanner',
hideFooter: 'ui.hideFooter',
showMemoryUsage: 'ui.showMemoryUsage',
showLineNumbers: 'ui.showLineNumbers',
accessibility: 'ui.accessibility',
ideMode: 'ide.enabled',
hasSeenIdeIntegrationNudge: 'ide.hasSeenNudge',
usageStatisticsEnabled: 'privacy.usageStatisticsEnabled',
telemetry: 'telemetry',
model: 'model.name',
maxSessionTurns: 'model.maxSessionTurns',
summarizeToolOutput: 'model.summarizeToolOutput',
chatCompression: 'model.chatCompression',
skipNextSpeakerCheck: 'model.skipNextSpeakerCheck',
contextFileName: 'context.fileName',
memoryImportFormat: 'context.importFormat',
memoryDiscoveryMaxDirs: 'context.discoveryMaxDirs',
includeDirectories: 'context.includeDirectories',
loadMemoryFromIncludeDirectories: 'context.loadFromIncludeDirectories',
fileFiltering: 'context.fileFiltering',
sandbox: 'tools.sandbox',
shouldUseNodePtyShell: 'tools.usePty',
coreTools: 'tools.core',
excludeTools: 'tools.exclude',
toolDiscoveryCommand: 'tools.discoveryCommand',
toolCallCommand: 'tools.callCommand',
mcpServerCommand: 'mcp.serverCommand',
allowMCPServers: 'mcp.allowed',
excludeMCPServers: 'mcp.excluded',
folderTrustFeature: 'security.folderTrust.featureEnabled',
folderTrust: 'security.folderTrust.enabled',
selectedAuthType: 'security.auth.selectedType',
useExternalAuth: 'security.auth.useExternal',
autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
dnsResolutionOrder: 'advanced.dnsResolutionOrder',
excludedProjectEnvVars: 'advanced.excludedEnvVars',
bugCommand: 'advanced.bugCommand',
};
export function getSystemSettingsPath(): string {
if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) {
return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
@@ -82,6 +135,134 @@ export interface SettingsFile {
path: string;
}
function setNestedProperty(
obj: Record<string, unknown>,
path: string,
value: unknown,
) {
const keys = path.split('.');
const lastKey = keys.pop();
if (!lastKey) return;
let current: Record<string, unknown> = obj;
for (const key of keys) {
if (current[key] === undefined) {
current[key] = {};
}
const next = current[key];
if (typeof next === 'object' && next !== null) {
current = next as Record<string, unknown>;
} else {
// This path is invalid, so we stop.
return;
}
}
current[lastKey] = value;
}
function needsMigration(settings: Record<string, unknown>): boolean {
return !('general' in settings);
}
function migrateSettingsToV2(
flatSettings: Record<string, unknown>,
): Record<string, unknown> | null {
if (!needsMigration(flatSettings)) {
return null;
}
const v2Settings: Record<string, unknown> = {};
const flatKeys = new Set(Object.keys(flatSettings));
for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) {
if (flatKeys.has(oldKey)) {
setNestedProperty(v2Settings, newPath, flatSettings[oldKey]);
flatKeys.delete(oldKey);
}
}
// Preserve mcpServers at the top level
if (flatSettings['mcpServers']) {
v2Settings['mcpServers'] = flatSettings['mcpServers'];
flatKeys.delete('mcpServers');
}
// Carry over any unrecognized keys
for (const remainingKey of flatKeys) {
v2Settings[remainingKey] = flatSettings[remainingKey];
}
return v2Settings;
}
function getNestedProperty(
obj: Record<string, unknown>,
path: string,
): unknown {
const keys = path.split('.');
let current: unknown = obj;
for (const key of keys) {
if (typeof current !== 'object' || current === null || !(key in current)) {
return undefined;
}
current = (current as Record<string, unknown>)[key];
}
return current;
}
const REVERSE_MIGRATION_MAP: Record<string, string> = Object.fromEntries(
Object.entries(MIGRATION_MAP).map(([key, value]) => [value, key]),
);
// Dynamically determine the top-level keys from the V2 settings structure.
const KNOWN_V2_CONTAINERS = new Set(
Object.values(MIGRATION_MAP).map((path) => path.split('.')[0]),
);
export function migrateSettingsToV1(
v2Settings: Record<string, unknown>,
): Record<string, unknown> {
const v1Settings: Record<string, unknown> = {};
const v2Keys = new Set(Object.keys(v2Settings));
for (const [newPath, oldKey] of Object.entries(REVERSE_MIGRATION_MAP)) {
const value = getNestedProperty(v2Settings, newPath);
if (value !== undefined) {
v1Settings[oldKey] = value;
v2Keys.delete(newPath.split('.')[0]);
}
}
// Preserve mcpServers at the top level
if (v2Settings['mcpServers']) {
v1Settings['mcpServers'] = v2Settings['mcpServers'];
v2Keys.delete('mcpServers');
}
// Carry over any unrecognized keys
for (const remainingKey of v2Keys) {
const value = v2Settings[remainingKey];
if (value === undefined) {
continue;
}
// Don't carry over empty objects that were just containers for migrated settings.
if (
KNOWN_V2_CONTAINERS.has(remainingKey) &&
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
Object.keys(value).length === 0
) {
continue;
}
v1Settings[remainingKey] = value;
}
return v1Settings;
}
function mergeSettings(
system: Settings,
systemDefaults: Settings,
@@ -92,8 +273,17 @@ function mergeSettings(
const safeWorkspace = isTrusted ? workspace : ({} as Settings);
// folderTrust is not supported at workspace level.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { folderTrust, ...safeWorkspaceWithoutFolderTrust } = safeWorkspace;
const { security, ...restOfWorkspace } = safeWorkspace;
const safeWorkspaceWithoutFolderTrust = security
? {
...restOfWorkspace,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
security: (({ folderTrust, ...rest }) => rest)(security),
}
: {
...restOfWorkspace,
security: {},
};
// Settings are merged with the following precedence (last one wins for
// single values):
@@ -109,40 +299,84 @@ function mergeSettings(
...user,
...safeWorkspaceWithoutFolderTrust,
...system,
customThemes: {
...(systemDefaults.customThemes || {}),
...(user.customThemes || {}),
...(safeWorkspace.customThemes || {}),
...(system.customThemes || {}),
ui: {
...(systemDefaults.ui || {}),
...(user.ui || {}),
...(safeWorkspaceWithoutFolderTrust.ui || {}),
...(system.ui || {}),
customThemes: {
...(systemDefaults.ui?.customThemes || {}),
...(user.ui?.customThemes || {}),
...(safeWorkspaceWithoutFolderTrust.ui?.customThemes || {}),
...(system.ui?.customThemes || {}),
},
},
security: {
...(systemDefaults.security || {}),
...(user.security || {}),
...(safeWorkspaceWithoutFolderTrust.security || {}),
...(system.security || {}),
},
mcp: {
...(systemDefaults.mcp || {}),
...(user.mcp || {}),
...(safeWorkspaceWithoutFolderTrust.mcp || {}),
...(system.mcp || {}),
},
mcpServers: {
...(systemDefaults.mcpServers || {}),
...(user.mcpServers || {}),
...(safeWorkspace.mcpServers || {}),
...(safeWorkspaceWithoutFolderTrust.mcpServers || {}),
...(system.mcpServers || {}),
},
includeDirectories: [
...(systemDefaults.includeDirectories || []),
...(user.includeDirectories || []),
...(safeWorkspace.includeDirectories || []),
...(system.includeDirectories || []),
],
chatCompression: {
...(systemDefaults.chatCompression || {}),
...(user.chatCompression || {}),
...(safeWorkspace.chatCompression || {}),
...(system.chatCompression || {}),
context: {
...(systemDefaults.context || {}),
...(user.context || {}),
...(safeWorkspaceWithoutFolderTrust.context || {}),
...(system.context || {}),
includeDirectories: [
...(systemDefaults.context?.includeDirectories || []),
...(user.context?.includeDirectories || []),
...(safeWorkspaceWithoutFolderTrust.context?.includeDirectories || []),
...(system.context?.includeDirectories || []),
],
},
model: {
...(systemDefaults.model || {}),
...(user.model || {}),
...(safeWorkspaceWithoutFolderTrust.model || {}),
...(system.model || {}),
chatCompression: {
...(systemDefaults.model?.chatCompression || {}),
...(user.model?.chatCompression || {}),
...(safeWorkspaceWithoutFolderTrust.model?.chatCompression || {}),
...(system.model?.chatCompression || {}),
},
},
advanced: {
...(systemDefaults.advanced || {}),
...(user.advanced || {}),
...(safeWorkspaceWithoutFolderTrust.advanced || {}),
...(system.advanced || {}),
excludedEnvVars: [
...new Set([
...(systemDefaults.advanced?.excludedEnvVars || []),
...(user.advanced?.excludedEnvVars || []),
...(safeWorkspaceWithoutFolderTrust.advanced?.excludedEnvVars || []),
...(system.advanced?.excludedEnvVars || []),
]),
],
},
extensions: {
...(systemDefaults.extensions || {}),
...(user.extensions || {}),
...(safeWorkspace.extensions || {}),
...(safeWorkspaceWithoutFolderTrust.extensions || {}),
...(system.extensions || {}),
disabled: [
...new Set([
...(systemDefaults.extensions?.disabled || []),
...(user.extensions?.disabled || []),
...(safeWorkspace.extensions?.disabled || []),
...(safeWorkspaceWithoutFolderTrust.extensions?.disabled || []),
...(system.extensions?.disabled || []),
]),
],
@@ -150,7 +384,8 @@ function mergeSettings(
...new Set([
...(systemDefaults.extensions?.workspacesWithMigrationNudge || []),
...(user.extensions?.workspacesWithMigrationNudge || []),
...(safeWorkspace.extensions?.workspacesWithMigrationNudge || []),
...(safeWorkspaceWithoutFolderTrust.extensions
?.workspacesWithMigrationNudge || []),
...(system.extensions?.workspacesWithMigrationNudge || []),
]),
],
@@ -166,6 +401,7 @@ export class LoadedSettings {
workspace: SettingsFile,
errors: SettingsError[],
isTrusted: boolean,
migratedInMemorScopes: Set<SettingScope>,
) {
this.system = system;
this.systemDefaults = systemDefaults;
@@ -173,6 +409,7 @@ export class LoadedSettings {
this.workspace = workspace;
this.errors = errors;
this.isTrusted = isTrusted;
this.migratedInMemorScopes = migratedInMemorScopes;
this._merged = this.computeMergedSettings();
}
@@ -182,6 +419,7 @@ export class LoadedSettings {
readonly workspace: SettingsFile;
readonly errors: SettingsError[];
readonly isTrusted: boolean;
readonly migratedInMemorScopes: Set<SettingScope>;
private _merged: Settings;
@@ -214,13 +452,9 @@ export class LoadedSettings {
}
}
setValue<K extends keyof Settings>(
scope: SettingScope,
key: K,
value: Settings[K],
): void {
setValue(scope: SettingScope, key: string, value: unknown): void {
const settingsFile = this.forScope(scope);
settingsFile.settings[key] = value;
setNestedProperty(settingsFile.settings, key, value);
this._merged = this.computeMergedSettings();
saveSettings(settingsFile);
}
@@ -357,7 +591,8 @@ export function loadEnvironment(settings?: Settings): void {
const parsedEnv = dotenv.parse(envFileContent);
const excludedVars =
resolvedSettings?.excludedProjectEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
resolvedSettings?.advanced?.excludedEnvVars ||
DEFAULT_EXCLUDED_ENV_VARS;
const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR);
for (const key in parsedEnv) {
@@ -391,6 +626,7 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
const settingsErrors: SettingsError[] = [];
const systemSettingsPath = getSystemSettingsPath();
const systemDefaultsPath = getSystemDefaultsPath();
const migratedInMemorScopes = new Set<SettingScope>();
// Resolve paths to their canonical representation to handle symlinks
const resolvedWorkspaceDir = path.resolve(workspaceDir);
@@ -411,85 +647,97 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
workspaceDir,
).getWorkspaceSettingsPath();
// Load system settings
try {
if (fs.existsSync(systemSettingsPath)) {
const systemContent = fs.readFileSync(systemSettingsPath, 'utf-8');
systemSettings = JSON.parse(stripJsonComments(systemContent)) as Settings;
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: systemSettingsPath,
});
}
// Load system defaults
try {
if (fs.existsSync(systemDefaultsPath)) {
const systemDefaultsContent = fs.readFileSync(
systemDefaultsPath,
'utf-8',
);
const parsedSystemDefaults = JSON.parse(
stripJsonComments(systemDefaultsContent),
) as Settings;
systemDefaultSettings = resolveEnvVarsInObject(parsedSystemDefaults);
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: systemDefaultsPath,
});
}
// Load user settings
try {
if (fs.existsSync(USER_SETTINGS_PATH)) {
const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
userSettings = JSON.parse(stripJsonComments(userContent)) as Settings;
// Support legacy theme names
if (userSettings.theme && userSettings.theme === 'VS') {
userSettings.theme = DefaultLight.name;
} else if (userSettings.theme && userSettings.theme === 'VS2015') {
userSettings.theme = DefaultDark.name;
}
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: USER_SETTINGS_PATH,
});
}
if (realWorkspaceDir !== realHomeDir) {
// Load workspace settings
const loadAndMigrate = (filePath: string, scope: SettingScope): Settings => {
try {
if (fs.existsSync(workspaceSettingsPath)) {
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
workspaceSettings = JSON.parse(
stripJsonComments(projectContent),
) as Settings;
if (workspaceSettings.theme && workspaceSettings.theme === 'VS') {
workspaceSettings.theme = DefaultLight.name;
} else if (
workspaceSettings.theme &&
workspaceSettings.theme === 'VS2015'
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
const rawSettings: unknown = JSON.parse(stripJsonComments(content));
if (
typeof rawSettings !== 'object' ||
rawSettings === null ||
Array.isArray(rawSettings)
) {
workspaceSettings.theme = DefaultDark.name;
settingsErrors.push({
message: 'Settings file is not a valid JSON object.',
path: filePath,
});
return {};
}
let settingsObject = rawSettings as Record<string, unknown>;
if (needsMigration(settingsObject)) {
console.error(`Legacy settings file detected at: ${filePath}`);
const migratedSettings = migrateSettingsToV2(settingsObject);
if (migratedSettings) {
if (MIGRATE_V2_OVERWRITE) {
try {
fs.renameSync(filePath, `${filePath}.orig`);
fs.writeFileSync(
filePath,
JSON.stringify(migratedSettings, null, 2),
'utf-8',
);
console.log(
`Successfully migrated and saved settings file: ${filePath}`,
);
} catch (e) {
console.error(
`Error migrating settings file on disk: ${getErrorMessage(
e,
)}`,
);
}
} else {
console.log(
`Successfully migrated settings for ${filePath} in-memory for the current session.`,
);
migratedInMemorScopes.add(scope);
}
settingsObject = migratedSettings;
}
}
return settingsObject as Settings;
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: workspaceSettingsPath,
path: filePath,
});
}
return {};
};
systemSettings = loadAndMigrate(systemSettingsPath, SettingScope.System);
systemDefaultSettings = loadAndMigrate(
systemDefaultsPath,
SettingScope.SystemDefaults,
);
userSettings = loadAndMigrate(USER_SETTINGS_PATH, SettingScope.User);
if (realWorkspaceDir !== realHomeDir) {
workspaceSettings = loadAndMigrate(
workspaceSettingsPath,
SettingScope.Workspace,
);
}
// Support legacy theme names
if (userSettings.ui?.theme === 'VS') {
userSettings.ui.theme = DefaultLight.name;
} else if (userSettings.ui?.theme === 'VS2015') {
userSettings.ui.theme = DefaultDark.name;
}
if (workspaceSettings.ui?.theme === 'VS') {
workspaceSettings.ui.theme = DefaultLight.name;
} else if (workspaceSettings.ui?.theme === 'VS2015') {
workspaceSettings.ui.theme = DefaultDark.name;
}
// For the initial trust check, we can only use user and system settings.
const initialTrustCheckSettings = { ...systemSettings, ...userSettings };
const isTrusted = isWorkspaceTrusted(initialTrustCheckSettings) ?? true;
const initialTrustCheckSettings = mergeWith({}, systemSettings, userSettings);
const isTrusted =
isWorkspaceTrusted(initialTrustCheckSettings as Settings) ?? true;
// Create a temporary merged settings object to pass to loadEnvironment.
const tempMergedSettings = mergeSettings(
@@ -529,21 +777,9 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
},
settingsErrors,
isTrusted,
migratedInMemorScopes,
);
// Validate chatCompression settings
const chatCompression = loadedSettings.merged.chatCompression;
const threshold = chatCompression?.contextPercentageThreshold;
if (
threshold != null &&
(typeof threshold !== 'number' || threshold < 0 || threshold > 1)
) {
console.warn(
`Invalid value for chatCompression.contextPercentageThreshold: "${threshold}". Please use a value between 0 and 1. Using default compression settings.`,
);
delete loadedSettings.merged.chatCompression;
}
return loadedSettings;
}
@@ -555,9 +791,16 @@ export function saveSettings(settingsFile: SettingsFile): void {
fs.mkdirSync(dirPath, { recursive: true });
}
let settingsToSave = settingsFile.settings;
if (!MIGRATE_V2_OVERWRITE) {
settingsToSave = migrateSettingsToV1(
settingsToSave as Record<string, unknown>,
) as Settings;
}
fs.writeFileSync(
settingsFile.path,
JSON.stringify(settingsFile.settings, null, 2),
JSON.stringify(settingsToSave, null, 2),
'utf-8',
);
} catch (error) {

View File

@@ -12,49 +12,18 @@ describe('SettingsSchema', () => {
describe('SETTINGS_SCHEMA', () => {
it('should contain all expected top-level settings', () => {
const expectedSettings = [
'theme',
'customThemes',
'showMemoryUsage',
'usageStatisticsEnabled',
'autoConfigureMaxOldSpaceSize',
'preferredEditor',
'maxSessionTurns',
'memoryImportFormat',
'memoryDiscoveryMaxDirs',
'contextFileName',
'vimMode',
'ideMode',
'accessibility',
'checkpointing',
'fileFiltering',
'disableAutoUpdate',
'hideWindowTitle',
'hideTips',
'hideBanner',
'selectedAuthType',
'useExternalAuth',
'sandbox',
'coreTools',
'excludeTools',
'toolDiscoveryCommand',
'toolCallCommand',
'mcpServerCommand',
'mcpServers',
'allowMCPServers',
'excludeMCPServers',
'general',
'ui',
'ide',
'privacy',
'telemetry',
'bugCommand',
'summarizeToolOutput',
'dnsResolutionOrder',
'excludedProjectEnvVars',
'disableUpdateNag',
'includeDirectories',
'loadMemoryFromIncludeDirectories',
'model',
'hasSeenIdeIntegrationNudge',
'folderTrustFeature',
'useRipgrep',
'debugKeystrokeLogging',
'context',
'tools',
'mcp',
'security',
'advanced',
];
expectedSettings.forEach((setting) => {
@@ -80,9 +49,16 @@ describe('SettingsSchema', () => {
it('should have correct nested setting structure', () => {
const nestedSettings = [
'accessibility',
'checkpointing',
'fileFiltering',
'general',
'ui',
'ide',
'privacy',
'model',
'context',
'tools',
'mcp',
'security',
'advanced',
];
nestedSettings.forEach((setting) => {
@@ -99,29 +75,36 @@ describe('SettingsSchema', () => {
it('should have accessibility nested properties', () => {
expect(
SETTINGS_SCHEMA.accessibility.properties?.disableLoadingPhrases,
SETTINGS_SCHEMA.ui?.properties?.accessibility?.properties,
).toBeDefined();
expect(
SETTINGS_SCHEMA.accessibility.properties?.disableLoadingPhrases.type,
SETTINGS_SCHEMA.ui?.properties?.accessibility.properties
?.disableLoadingPhrases.type,
).toBe('boolean');
});
it('should have checkpointing nested properties', () => {
expect(SETTINGS_SCHEMA.checkpointing.properties?.enabled).toBeDefined();
expect(SETTINGS_SCHEMA.checkpointing.properties?.enabled.type).toBe(
'boolean',
);
expect(
SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled,
).toBeDefined();
expect(
SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled
.type,
).toBe('boolean');
});
it('should have fileFiltering nested properties', () => {
expect(
SETTINGS_SCHEMA.fileFiltering.properties?.respectGitIgnore,
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
?.respectGitIgnore,
).toBeDefined();
expect(
SETTINGS_SCHEMA.fileFiltering.properties?.respectGeminiIgnore,
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
?.respectGeminiIgnore,
).toBeDefined();
expect(
SETTINGS_SCHEMA.fileFiltering.properties?.enableRecursiveFileSearch,
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
?.enableRecursiveFileSearch,
).toBeDefined();
});
@@ -150,11 +133,6 @@ describe('SettingsSchema', () => {
expect(categories.size).toBeGreaterThan(0);
expect(categories).toContain('General');
expect(categories).toContain('UI');
expect(categories).toContain('Mode');
expect(categories).toContain('Updates');
expect(categories).toContain('Accessibility');
expect(categories).toContain('Checkpointing');
expect(categories).toContain('File Filtering');
expect(categories).toContain('Advanced');
});
@@ -183,85 +161,148 @@ describe('SettingsSchema', () => {
it('should have showInDialog property configured', () => {
// Check that user-facing settings are marked for dialog display
expect(SETTINGS_SCHEMA.showMemoryUsage.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.vimMode.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.ideMode.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.disableAutoUpdate.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.hideWindowTitle.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.hideTips.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.hideBanner.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.usageStatisticsEnabled.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.ui.properties.showMemoryUsage.showInDialog).toBe(
true,
);
expect(SETTINGS_SCHEMA.general.properties.vimMode.showInDialog).toBe(
true,
);
expect(SETTINGS_SCHEMA.ide.properties.enabled.showInDialog).toBe(true);
expect(
SETTINGS_SCHEMA.general.properties.disableAutoUpdate.showInDialog,
).toBe(true);
expect(SETTINGS_SCHEMA.ui.properties.hideWindowTitle.showInDialog).toBe(
true,
);
expect(SETTINGS_SCHEMA.ui.properties.hideTips.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.ui.properties.hideBanner.showInDialog).toBe(true);
expect(
SETTINGS_SCHEMA.privacy.properties.usageStatisticsEnabled.showInDialog,
).toBe(false);
// Check that advanced settings are hidden from dialog
expect(SETTINGS_SCHEMA.selectedAuthType.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.coreTools.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.security.properties.auth.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.tools.properties.core.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.mcpServers.showInDialog).toBe(false);
expect(SETTINGS_SCHEMA.telemetry.showInDialog).toBe(false);
// Check that some settings are appropriately hidden
expect(SETTINGS_SCHEMA.theme.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.customThemes.showInDialog).toBe(false); // Managed via theme editor
expect(SETTINGS_SCHEMA.checkpointing.showInDialog).toBe(false); // Experimental feature
expect(SETTINGS_SCHEMA.accessibility.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.fileFiltering.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.preferredEditor.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.autoConfigureMaxOldSpaceSize.showInDialog).toBe(
true,
);
expect(SETTINGS_SCHEMA.ui.properties.theme.showInDialog).toBe(false); // Changed to false
expect(SETTINGS_SCHEMA.ui.properties.customThemes.showInDialog).toBe(
false,
); // Managed via theme editor
expect(
SETTINGS_SCHEMA.general.properties.checkpointing.showInDialog,
).toBe(false); // Experimental feature
expect(SETTINGS_SCHEMA.ui.properties.accessibility.showInDialog).toBe(
false,
); // Changed to false
expect(
SETTINGS_SCHEMA.context.properties.fileFiltering.showInDialog,
).toBe(false); // Changed to false
expect(
SETTINGS_SCHEMA.general.properties.preferredEditor.showInDialog,
).toBe(false); // Changed to false
expect(
SETTINGS_SCHEMA.advanced.properties.autoConfigureMemory.showInDialog,
).toBe(false);
});
it('should infer Settings type correctly', () => {
// This test ensures that the Settings type is properly inferred from the schema
const settings: Settings = {
theme: 'dark',
includeDirectories: ['/path/to/dir'],
loadMemoryFromIncludeDirectories: true,
ui: {
theme: 'dark',
},
context: {
includeDirectories: ['/path/to/dir'],
loadMemoryFromIncludeDirectories: true,
},
};
// TypeScript should not complain about these properties
expect(settings.theme).toBe('dark');
expect(settings.includeDirectories).toEqual(['/path/to/dir']);
expect(settings.loadMemoryFromIncludeDirectories).toBe(true);
expect(settings.ui?.theme).toBe('dark');
expect(settings.context?.includeDirectories).toEqual(['/path/to/dir']);
expect(settings.context?.loadMemoryFromIncludeDirectories).toBe(true);
});
it('should have includeDirectories setting in schema', () => {
expect(SETTINGS_SCHEMA.includeDirectories).toBeDefined();
expect(SETTINGS_SCHEMA.includeDirectories.type).toBe('array');
expect(SETTINGS_SCHEMA.includeDirectories.category).toBe('General');
expect(SETTINGS_SCHEMA.includeDirectories.default).toEqual([]);
expect(
SETTINGS_SCHEMA.context?.properties.includeDirectories,
).toBeDefined();
expect(SETTINGS_SCHEMA.context?.properties.includeDirectories.type).toBe(
'array',
);
expect(
SETTINGS_SCHEMA.context?.properties.includeDirectories.category,
).toBe('Context');
expect(
SETTINGS_SCHEMA.context?.properties.includeDirectories.default,
).toEqual([]);
});
it('should have loadMemoryFromIncludeDirectories setting in schema', () => {
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories).toBeDefined();
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.type).toBe(
'boolean',
);
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.category).toBe(
'General',
);
expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.default).toBe(
false,
);
expect(
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories,
).toBeDefined();
expect(
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
.type,
).toBe('boolean');
expect(
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
.category,
).toBe('Context');
expect(
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
.default,
).toBe(false);
});
it('should have folderTrustFeature setting in schema', () => {
expect(SETTINGS_SCHEMA.folderTrustFeature).toBeDefined();
expect(SETTINGS_SCHEMA.folderTrustFeature.type).toBe('boolean');
expect(SETTINGS_SCHEMA.folderTrustFeature.category).toBe('General');
expect(SETTINGS_SCHEMA.folderTrustFeature.default).toBe(false);
expect(SETTINGS_SCHEMA.folderTrustFeature.showInDialog).toBe(true);
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled,
).toBeDefined();
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled.type,
).toBe('boolean');
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
.category,
).toBe('Security');
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
.default,
).toBe(false);
expect(
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
.showInDialog,
).toBe(true);
});
it('should have debugKeystrokeLogging setting in schema', () => {
expect(SETTINGS_SCHEMA.debugKeystrokeLogging).toBeDefined();
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.type).toBe('boolean');
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.category).toBe('General');
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.default).toBe(false);
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.requiresRestart).toBe(false);
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.showInDialog).toBe(true);
expect(SETTINGS_SCHEMA.debugKeystrokeLogging.description).toBe(
'Enable debug logging of keystrokes to the console.',
);
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging,
).toBeDefined();
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.type,
).toBe('boolean');
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.category,
).toBe('General');
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.default,
).toBe(false);
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging
.requiresRestart,
).toBe(false);
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.showInDialog,
).toBe(true);
expect(
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.description,
).toBe('Enable debug logging of keystrokes to the console.');
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -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(() => {

View File

@@ -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) {

View File

@@ -152,6 +152,7 @@ describe('gemini.tsx main function', () => {
workspaceSettingsFile,
[settingsError],
true,
new Set(),
);
loadSettingsMock.mockReturnValue(mockLoadedSettings);
@@ -241,7 +242,9 @@ describe('startInteractiveUI', () => {
} as Config;
const mockSettings = {
merged: {
hideWindowTitle: false,
ui: {
hideWindowTitle: false,
},
},
} as LoadedSettings;
const mockStartupWarnings = ['warning1'];

View File

@@ -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(

View File

@@ -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();
});
});

View File

@@ -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}

View File

@@ -32,7 +32,11 @@ describe('aboutCommand', () => {
},
settings: {
merged: {
selectedAuthType: 'test-auth',
security: {
auth: {
selectedType: 'test-auth',
},
},
},
},
},

View File

@@ -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() &&

View File

@@ -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);

View File

@@ -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(

View File

@@ -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);

View File

@@ -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(

View File

@@ -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.',

View File

@@ -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 (

View File

@@ -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(

View File

@@ -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);

View File

@@ -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,
);

View File

@@ -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]);

View File

@@ -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()

View File

@@ -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,

View File

@@ -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];

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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(

View File

@@ -64,7 +64,9 @@ describe('handleAutoUpdate', () => {
mockSettings = {
merged: {
disableAutoUpdate: false,
general: {
disableAutoUpdate: false,
},
},
} as LoadedSettings;
@@ -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.',

View File

@@ -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');

View File

@@ -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,
);

View File

@@ -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);
}
});
}

View File

@@ -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}`);

View File

@@ -5,6 +5,7 @@
*/
export * from './src/index.js';
export { Storage } from './src/config/storage.js';
export {
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,