mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
Make merged settings non-nullable and fix all lints related to that. (#16647)
This commit is contained in:
@@ -230,8 +230,7 @@ export async function handleMigrateFromClaude() {
|
||||
const settings = loadSettings(workingDir);
|
||||
|
||||
// Merge migrated hooks with existing hooks
|
||||
const existingHooks =
|
||||
(settings.merged.hooks as Record<string, unknown>) || {};
|
||||
const existingHooks = settings.merged.hooks as Record<string, unknown>;
|
||||
const mergedHooks = { ...existingHooks, ...migratedHooks };
|
||||
|
||||
// Update settings (setValue automatically saves)
|
||||
|
||||
@@ -6,15 +6,20 @@
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
|
||||
import { listMcpServers } from './list.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { loadSettings, mergeSettings } from '../../config/settings.js';
|
||||
import { createTransport, debugLogger } from '@google/gemini-cli-core';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { ExtensionStorage } from '../../config/extensions/storage.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
vi.mock('../../config/settings.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../config/settings.js')>();
|
||||
return {
|
||||
...actual,
|
||||
loadSettings: vi.fn(),
|
||||
}));
|
||||
};
|
||||
});
|
||||
vi.mock('../../config/extensions/storage.js', () => ({
|
||||
ExtensionStorage: {
|
||||
getUserExtensionsDir: vi.fn(),
|
||||
@@ -32,11 +37,16 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
CONNECTING: 'CONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
},
|
||||
Storage: vi.fn().mockImplementation((_cwd: string) => ({
|
||||
Storage: Object.assign(
|
||||
vi.fn().mockImplementation((_cwd: string) => ({
|
||||
getGlobalSettingsPath: () => '/tmp/gemini/settings.json',
|
||||
getWorkspaceSettingsPath: () => '/tmp/gemini/workspace-settings.json',
|
||||
getProjectTempDir: () => '/test/home/.gemini/tmp/mocked_hash',
|
||||
})),
|
||||
{
|
||||
getGlobalSettingsPath: () => '/tmp/gemini/settings.json',
|
||||
},
|
||||
),
|
||||
GEMINI_DIR: '.gemini',
|
||||
getErrorMessage: (e: unknown) =>
|
||||
e instanceof Error ? e.message : String(e),
|
||||
@@ -96,7 +106,10 @@ describe('mcp list command', () => {
|
||||
});
|
||||
|
||||
it('should display message when no servers configured', async () => {
|
||||
mockedLoadSettings.mockReturnValue({ merged: { mcpServers: {} } });
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
mockedLoadSettings.mockReturnValue({
|
||||
merged: { ...defaultMergedSettings, mcpServers: {} },
|
||||
});
|
||||
|
||||
await listMcpServers();
|
||||
|
||||
@@ -104,8 +117,10 @@ describe('mcp list command', () => {
|
||||
});
|
||||
|
||||
it('should display different server types with connected status', async () => {
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
mockedLoadSettings.mockReturnValue({
|
||||
merged: {
|
||||
...defaultMergedSettings,
|
||||
mcpServers: {
|
||||
'stdio-server': { command: '/path/to/server', args: ['arg1'] },
|
||||
'sse-server': { url: 'https://example.com/sse' },
|
||||
@@ -138,8 +153,10 @@ describe('mcp list command', () => {
|
||||
});
|
||||
|
||||
it('should display disconnected status when connection fails', async () => {
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
mockedLoadSettings.mockReturnValue({
|
||||
merged: {
|
||||
...defaultMergedSettings,
|
||||
mcpServers: {
|
||||
'test-server': { command: '/test/server' },
|
||||
},
|
||||
@@ -158,9 +175,13 @@ describe('mcp list command', () => {
|
||||
});
|
||||
|
||||
it('should merge extension servers with config servers', async () => {
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
mockedLoadSettings.mockReturnValue({
|
||||
merged: {
|
||||
mcpServers: { 'config-server': { command: '/config/server' } },
|
||||
...defaultMergedSettings,
|
||||
mcpServers: {
|
||||
'config-server': { command: '/config/server' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ async function getMcpServersFromConfig(): Promise<
|
||||
requestSetting: promptForSetting,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const mcpServers = { ...(settings.merged.mcpServers || {}) };
|
||||
const mcpServers = { ...settings.merged.mcpServers };
|
||||
for (const extension of extensions) {
|
||||
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||
if (mcpServers[key]) {
|
||||
@@ -63,8 +63,7 @@ async function testMCPConnection(
|
||||
const sanitizationConfig = {
|
||||
enableEnvironmentVariableRedaction: true,
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables:
|
||||
settings.merged.advanced?.excludedEnvVars || [],
|
||||
blockedEnvironmentVariables: settings.merged.advanced.excludedEnvVars,
|
||||
};
|
||||
|
||||
let transport;
|
||||
|
||||
@@ -22,8 +22,9 @@ import {
|
||||
Config,
|
||||
DEFAULT_FILE_FILTERING_OPTIONS,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Settings } from './settingsSchema.js';
|
||||
import { createTestMergedSettings } from './settings.js';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
export const server = setupServer();
|
||||
@@ -212,7 +213,7 @@ describe('Configuration Integration Tests', () => {
|
||||
const originalArgv = process.argv;
|
||||
try {
|
||||
process.argv = argv;
|
||||
const parsedArgs = await parseArguments({} as Settings);
|
||||
const parsedArgs = await parseArguments(createTestMergedSettings());
|
||||
expect(parsedArgs.approvalMode).toBe(expected.approvalMode);
|
||||
expect(parsedArgs.prompt).toBe(expected.prompt);
|
||||
expect(parsedArgs.yolo).toBe(expected.yolo);
|
||||
@@ -235,7 +236,9 @@ describe('Configuration Integration Tests', () => {
|
||||
const originalArgv = process.argv;
|
||||
try {
|
||||
process.argv = argv;
|
||||
await expect(parseArguments({} as Settings)).rejects.toThrow();
|
||||
await expect(
|
||||
parseArguments(createTestMergedSettings()),
|
||||
).rejects.toThrow();
|
||||
} finally {
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,8 +38,12 @@ import {
|
||||
type OutputFormat,
|
||||
GEMINI_MODEL_ALIAS_AUTO,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Settings } from './settings.js';
|
||||
import { saveModelChange, loadSettings } from './settings.js';
|
||||
import {
|
||||
type Settings,
|
||||
type MergedSettings,
|
||||
saveModelChange,
|
||||
loadSettings,
|
||||
} from './settings.js';
|
||||
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
import { resolvePath } from '../utils/resolvePath.js';
|
||||
@@ -54,7 +58,6 @@ import { requestConsentNonInteractive } from './extensions/consent.js';
|
||||
import { promptForSetting } from './extensions/extensionSettings.js';
|
||||
import type { EventEmitter } from 'node:stream';
|
||||
import { runExitCleanup } from '../utils/cleanup.js';
|
||||
import { getEnableHooks, getEnableHooksUI } from './settingsSchema.js';
|
||||
|
||||
export interface CliArgs {
|
||||
query: string | undefined;
|
||||
@@ -82,7 +85,9 @@ export interface CliArgs {
|
||||
recordResponses: string | undefined;
|
||||
}
|
||||
|
||||
export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
export async function parseArguments(
|
||||
settings: MergedSettings,
|
||||
): Promise<CliArgs> {
|
||||
const rawArgv = hideBin(process.argv);
|
||||
const yargsInstance = yargs(rawArgv)
|
||||
.locale('en')
|
||||
@@ -280,16 +285,16 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
return true;
|
||||
});
|
||||
|
||||
if (settings?.experimental?.extensionManagement ?? true) {
|
||||
if (settings.experimental.extensionManagement) {
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
if (settings?.experimental?.skills ?? false) {
|
||||
if (settings.experimental.skills) {
|
||||
yargsInstance.command(skillsCommand);
|
||||
}
|
||||
|
||||
// Register hooks command if hooks are enabled
|
||||
if (getEnableHooksUI(settings)) {
|
||||
if (settings.tools.enableHooks) {
|
||||
yargsInstance.command(hooksCommand);
|
||||
}
|
||||
|
||||
@@ -392,7 +397,7 @@ export interface LoadCliConfigOptions {
|
||||
}
|
||||
|
||||
export async function loadCliConfig(
|
||||
settings: Settings,
|
||||
settings: MergedSettings,
|
||||
sessionId: string,
|
||||
argv: CliArgs,
|
||||
options: LoadCliConfigOptions = {},
|
||||
@@ -590,10 +595,7 @@ export async function loadCliConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const excludeTools = mergeExcludeTools(
|
||||
settings,
|
||||
extraExcludes.length > 0 ? extraExcludes : undefined,
|
||||
);
|
||||
const excludeTools = mergeExcludeTools(settings, extraExcludes);
|
||||
|
||||
// Create a settings object that includes CLI overrides for policy generation
|
||||
const effectiveSettings: Settings = {
|
||||
@@ -742,15 +744,17 @@ export async function loadCliConfig(
|
||||
disableLLMCorrection: settings.tools?.disableLLMCorrection,
|
||||
modelConfigServiceConfig: settings.modelConfigs,
|
||||
// TODO: loading of hooks based on workspace trust
|
||||
enableHooks: getEnableHooks(settings),
|
||||
enableHooksUI: getEnableHooksUI(settings),
|
||||
enableHooks:
|
||||
(settings.tools?.enableHooks ?? true) &&
|
||||
(settings.hooks?.enabled ?? false),
|
||||
enableHooksUI: settings.tools?.enableHooks ?? true,
|
||||
hooks: settings.hooks || {},
|
||||
projectHooks: projectHooks || {},
|
||||
onModelChange: (model: string) => saveModelChange(loadedSettings, model),
|
||||
onReload: async () => {
|
||||
const refreshedSettings = loadSettings(cwd);
|
||||
return {
|
||||
disabledSkills: refreshedSettings.merged.skills?.disabled,
|
||||
disabledSkills: refreshedSettings.merged.skills.disabled,
|
||||
agents: refreshedSettings.merged.agents,
|
||||
};
|
||||
},
|
||||
@@ -758,12 +762,12 @@ export async function loadCliConfig(
|
||||
}
|
||||
|
||||
function mergeExcludeTools(
|
||||
settings: Settings,
|
||||
extraExcludes?: string[] | undefined,
|
||||
settings: MergedSettings,
|
||||
extraExcludes: string[] = [],
|
||||
): string[] {
|
||||
const allExcludeTools = new Set([
|
||||
...(settings.tools?.exclude || []),
|
||||
...(extraExcludes || []),
|
||||
...(settings.tools.exclude || []),
|
||||
...extraExcludes,
|
||||
]);
|
||||
return [...allExcludeTools];
|
||||
return Array.from(allExcludeTools);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { type Settings } from './settings.js';
|
||||
import { createTestMergedSettings } from './settings.js';
|
||||
import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
|
||||
|
||||
@@ -52,10 +52,9 @@ describe('ExtensionManager agents loading', () => {
|
||||
fs.mkdirSync(extensionsDir, { recursive: true });
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
settings: {
|
||||
settings: createTestMergedSettings({
|
||||
telemetry: { enabled: false },
|
||||
trustedFolders: [tempDir],
|
||||
} as unknown as Settings,
|
||||
}),
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: vi.fn(),
|
||||
workspaceDir: tempDir,
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import type { Settings } from './settings.js';
|
||||
import { createTestMergedSettings } from './settings.js';
|
||||
import {
|
||||
loadAgentsFromDirectory,
|
||||
loadSkillsFromDir,
|
||||
@@ -105,14 +105,10 @@ describe('ExtensionManager Settings Scope', () => {
|
||||
workspaceDir: tempWorkspace,
|
||||
requestConsent: async () => true,
|
||||
requestSetting: async () => '',
|
||||
settings: {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: {
|
||||
extensionConfig: true,
|
||||
},
|
||||
} as Settings,
|
||||
settings: createTestMergedSettings({
|
||||
telemetry: { enabled: false },
|
||||
experimental: { extensionConfig: true },
|
||||
}),
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
@@ -147,14 +143,10 @@ describe('ExtensionManager Settings Scope', () => {
|
||||
workspaceDir: tempWorkspace,
|
||||
requestConsent: async () => true,
|
||||
requestSetting: async () => '',
|
||||
settings: {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: {
|
||||
extensionConfig: true,
|
||||
},
|
||||
} as Settings,
|
||||
settings: createTestMergedSettings({
|
||||
telemetry: { enabled: false },
|
||||
experimental: { extensionConfig: true },
|
||||
}),
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
@@ -187,14 +179,10 @@ describe('ExtensionManager Settings Scope', () => {
|
||||
workspaceDir: tempWorkspace,
|
||||
requestConsent: async () => true,
|
||||
requestSetting: async () => '',
|
||||
settings: {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
experimental: {
|
||||
extensionConfig: true,
|
||||
},
|
||||
} as Settings,
|
||||
settings: createTestMergedSettings({
|
||||
telemetry: { enabled: false },
|
||||
experimental: { extensionConfig: true },
|
||||
}),
|
||||
});
|
||||
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { debugLogger, coreEvents } from '@google/gemini-cli-core';
|
||||
import { type Settings } from './settings.js';
|
||||
import { createTestMergedSettings } from './settings.js';
|
||||
import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
|
||||
|
||||
@@ -58,10 +58,9 @@ describe('ExtensionManager skills validation', () => {
|
||||
fs.mkdirSync(extensionsDir, { recursive: true });
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
settings: {
|
||||
settings: createTestMergedSettings({
|
||||
telemetry: { enabled: false },
|
||||
trustedFolders: [tempDir],
|
||||
} as unknown as Settings,
|
||||
}),
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: vi.fn(),
|
||||
workspaceDir: tempDir,
|
||||
@@ -134,10 +133,9 @@ describe('ExtensionManager skills validation', () => {
|
||||
|
||||
// 3. Create a fresh ExtensionManager to force loading from disk
|
||||
const newExtensionManager = new ExtensionManager({
|
||||
settings: {
|
||||
settings: createTestMergedSettings({
|
||||
telemetry: { enabled: false },
|
||||
trustedFolders: [tempDir],
|
||||
} as unknown as Settings,
|
||||
}),
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: vi.fn(),
|
||||
workspaceDir: tempDir,
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as path from 'node:path';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import chalk from 'chalk';
|
||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
import { type Settings, SettingScope } from './settings.js';
|
||||
import { type MergedSettings, SettingScope } from './settings.js';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
|
||||
import {
|
||||
@@ -68,11 +68,10 @@ import {
|
||||
ExtensionSettingScope,
|
||||
} from './extensions/extensionSettings.js';
|
||||
import type { EventEmitter } from 'node:stream';
|
||||
import { getEnableHooks } from './settingsSchema.js';
|
||||
|
||||
interface ExtensionManagerParams {
|
||||
enabledExtensionOverrides?: string[];
|
||||
settings: Settings;
|
||||
settings: MergedSettings;
|
||||
requestConsent: (consent: string) => Promise<boolean>;
|
||||
requestSetting: ((setting: ExtensionSetting) => Promise<string>) | null;
|
||||
workspaceDir: string;
|
||||
@@ -86,7 +85,7 @@ interface ExtensionManagerParams {
|
||||
*/
|
||||
export class ExtensionManager extends ExtensionLoader {
|
||||
private extensionEnablementManager: ExtensionEnablementManager;
|
||||
private settings: Settings;
|
||||
private settings: MergedSettings;
|
||||
private requestConsent: (consent: string) => Promise<boolean>;
|
||||
private requestSetting:
|
||||
| ((setting: ExtensionSetting) => Promise<string>)
|
||||
@@ -143,7 +142,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
if (
|
||||
(installMetadata.type === 'git' ||
|
||||
installMetadata.type === 'github-release') &&
|
||||
this.settings.security?.blockGitExtensions
|
||||
this.settings.security.blockGitExtensions
|
||||
) {
|
||||
throw new Error(
|
||||
'Installing extensions from remote sources is disallowed by your current settings.',
|
||||
@@ -287,10 +286,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(destinationPath, { recursive: true });
|
||||
if (
|
||||
this.requestSetting &&
|
||||
(this.settings.experimental?.extensionConfig ?? false)
|
||||
) {
|
||||
if (this.requestSetting && this.settings.experimental.extensionConfig) {
|
||||
if (isUpdate) {
|
||||
await maybePromptForSettings(
|
||||
newExtensionConfig,
|
||||
@@ -308,8 +304,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
}
|
||||
|
||||
const missingSettings =
|
||||
(this.settings.experimental?.extensionConfig ?? false)
|
||||
const missingSettings = this.settings.experimental.extensionConfig
|
||||
? await getMissingSettings(
|
||||
newExtensionConfig,
|
||||
extensionId,
|
||||
@@ -478,7 +473,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
throw new Error('Extensions already loaded, only load extensions once.');
|
||||
}
|
||||
|
||||
if (this.settings.admin?.extensions?.enabled === false) {
|
||||
if (this.settings.admin.extensions.enabled === false) {
|
||||
this.loadedExtensions = [];
|
||||
return this.loadedExtensions;
|
||||
}
|
||||
@@ -511,7 +506,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
if (
|
||||
(installMetadata?.type === 'git' ||
|
||||
installMetadata?.type === 'github-release') &&
|
||||
this.settings.security?.blockGitExtensions
|
||||
this.settings.security.blockGitExtensions
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -535,7 +530,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
let userSettings: Record<string, string> = {};
|
||||
let workspaceSettings: Record<string, string> = {};
|
||||
|
||||
if (this.settings.experimental?.extensionConfig ?? false) {
|
||||
if (this.settings.experimental.extensionConfig) {
|
||||
userSettings = await getScopedEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
@@ -553,10 +548,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
config = resolveEnvVarsInObject(config, customEnv);
|
||||
|
||||
const resolvedSettings: ResolvedExtensionSetting[] = [];
|
||||
if (
|
||||
config.settings &&
|
||||
(this.settings.experimental?.extensionConfig ?? false)
|
||||
) {
|
||||
if (config.settings && this.settings.experimental.extensionConfig) {
|
||||
for (const setting of config.settings) {
|
||||
const value = customEnv[setting.envVar];
|
||||
let scope: 'user' | 'workspace' | undefined;
|
||||
@@ -600,7 +592,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
|
||||
if (config.mcpServers) {
|
||||
if (this.settings.admin?.mcp?.enabled === false) {
|
||||
if (this.settings.admin.mcp.enabled === false) {
|
||||
config.mcpServers = undefined;
|
||||
} else {
|
||||
config.mcpServers = Object.fromEntries(
|
||||
@@ -619,7 +611,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
.filter((contextFilePath) => fs.existsSync(contextFilePath));
|
||||
|
||||
let hooks: { [K in HookEventName]?: HookDefinition[] } | undefined;
|
||||
if (getEnableHooks(this.settings)) {
|
||||
if (this.settings.tools.enableHooks && this.settings.hooks.enabled) {
|
||||
hooks = await this.loadExtensionHooks(effectiveExtensionPath, {
|
||||
extensionPath: effectiveExtensionPath,
|
||||
workspacePath: this.workspaceDir,
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
loadAgentsFromDirectory,
|
||||
loadSkillsFromDir,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { loadSettings, SettingScope } from './settings.js';
|
||||
import {
|
||||
loadSettings,
|
||||
createTestMergedSettings,
|
||||
SettingScope,
|
||||
} from './settings.js';
|
||||
import {
|
||||
isWorkspaceTrusted,
|
||||
resetTrustedFoldersForTesting,
|
||||
@@ -201,7 +205,7 @@ describe('extension tests', () => {
|
||||
});
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||
const settings = loadSettings(tempWorkspaceDir).merged;
|
||||
(settings.experimental ??= {}).extensionConfig = true;
|
||||
settings.experimental.extensionConfig = true;
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
@@ -628,11 +632,9 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const blockGitExtensionsSetting = {
|
||||
security: {
|
||||
blockGitExtensions: true,
|
||||
},
|
||||
};
|
||||
const blockGitExtensionsSetting = createTestMergedSettings({
|
||||
security: { blockGitExtensions: true },
|
||||
});
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
@@ -652,7 +654,6 @@ describe('extension tests', () => {
|
||||
version: '1.0.0',
|
||||
});
|
||||
const loadedSettings = loadSettings(tempWorkspaceDir).merged;
|
||||
(loadedSettings.admin ??= {}).extensions ??= {};
|
||||
loadedSettings.admin.extensions.enabled = false;
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
@@ -676,7 +677,6 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
const loadedSettings = loadSettings(tempWorkspaceDir).merged;
|
||||
(loadedSettings.admin ??= {}).mcp ??= {};
|
||||
loadedSettings.admin.mcp.enabled = false;
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
@@ -701,7 +701,6 @@ describe('extension tests', () => {
|
||||
},
|
||||
});
|
||||
const loadedSettings = loadSettings(tempWorkspaceDir).merged;
|
||||
(loadedSettings.admin ??= {}).mcp ??= {};
|
||||
loadedSettings.admin.mcp.enabled = true;
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
@@ -837,7 +836,6 @@ describe('extension tests', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(tempWorkspaceDir).merged;
|
||||
if (!settings.hooks) settings.hooks = {};
|
||||
settings.hooks.enabled = true;
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
@@ -873,7 +871,6 @@ describe('extension tests', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(tempWorkspaceDir).merged;
|
||||
if (!settings.hooks) settings.hooks = {};
|
||||
settings.hooks.enabled = false;
|
||||
|
||||
extensionManager = new ExtensionManager({
|
||||
@@ -1098,11 +1095,9 @@ describe('extension tests', () => {
|
||||
|
||||
it('should not install a github extension if blockGitExtensions is set', async () => {
|
||||
const gitUrl = 'https://somehost.com/somerepo.git';
|
||||
const blockGitExtensionsSetting = {
|
||||
security: {
|
||||
blockGitExtensions: true,
|
||||
},
|
||||
};
|
||||
const blockGitExtensionsSetting = createTestMergedSettings({
|
||||
security: { blockGitExtensions: true },
|
||||
});
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
|
||||
@@ -11,7 +11,6 @@ import * as fs from 'node:fs';
|
||||
import { getMissingSettings } from './extensionSettings.js';
|
||||
import type { ExtensionConfig } from '../extension.js';
|
||||
import { ExtensionStorage } from './storage.js';
|
||||
import type { Settings } from '../settings.js';
|
||||
import {
|
||||
KeychainTokenStorage,
|
||||
debugLogger,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
} from '@google/gemini-cli-core';
|
||||
import { EXTENSION_SETTINGS_FILENAME } from './variables.js';
|
||||
import { ExtensionManager } from '../extension-manager.js';
|
||||
import { createTestMergedSettings } from '../settings.js';
|
||||
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -247,12 +247,10 @@ describe('extensionUpdates', () => {
|
||||
const manager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
|
||||
settings: {
|
||||
telemetry: {
|
||||
enabled: false,
|
||||
},
|
||||
settings: createTestMergedSettings({
|
||||
telemetry: { enabled: false },
|
||||
experimental: { extensionConfig: true },
|
||||
} as unknown as Settings,
|
||||
}),
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: null, // Simulate non-interactive
|
||||
});
|
||||
|
||||
@@ -24,12 +24,24 @@ import { DefaultDark } from '../ui/themes/default.js';
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import {
|
||||
type Settings,
|
||||
type MergedSettings,
|
||||
type MemoryImportFormat,
|
||||
type MergeStrategy,
|
||||
type SettingsSchema,
|
||||
type SettingDefinition,
|
||||
getSettingsSchema,
|
||||
} from './settingsSchema.js';
|
||||
|
||||
export {
|
||||
type Settings,
|
||||
type MergedSettings,
|
||||
type MemoryImportFormat,
|
||||
type MergeStrategy,
|
||||
type SettingsSchema,
|
||||
type SettingDefinition,
|
||||
getSettingsSchema,
|
||||
};
|
||||
|
||||
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
|
||||
import { customDeepMerge } from '../utils/deepMerge.js';
|
||||
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
|
||||
@@ -59,8 +71,6 @@ function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined {
|
||||
return current?.mergeStrategy;
|
||||
}
|
||||
|
||||
export type { Settings, MemoryImportFormat };
|
||||
|
||||
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'];
|
||||
@@ -201,10 +211,7 @@ export function getDefaultsFromSchema(
|
||||
for (const key in schema) {
|
||||
const definition = schema[key];
|
||||
if (definition.properties) {
|
||||
const childDefaults = getDefaultsFromSchema(definition.properties);
|
||||
if (Object.keys(childDefaults).length > 0) {
|
||||
defaults[key] = childDefaults;
|
||||
}
|
||||
defaults[key] = getDefaultsFromSchema(definition.properties);
|
||||
} else if (definition.default !== undefined) {
|
||||
defaults[key] = definition.default;
|
||||
}
|
||||
@@ -212,13 +219,13 @@ export function getDefaultsFromSchema(
|
||||
return defaults as Settings;
|
||||
}
|
||||
|
||||
function mergeSettings(
|
||||
export function mergeSettings(
|
||||
system: Settings,
|
||||
systemDefaults: Settings,
|
||||
user: Settings,
|
||||
workspace: Settings,
|
||||
isTrusted: boolean,
|
||||
): Settings {
|
||||
): MergedSettings {
|
||||
const safeWorkspace = isTrusted ? workspace : ({} as Settings);
|
||||
const schemaDefaults = getDefaultsFromSchema();
|
||||
|
||||
@@ -236,7 +243,24 @@ function mergeSettings(
|
||||
user,
|
||||
safeWorkspace,
|
||||
system,
|
||||
) as Settings;
|
||||
) as MergedSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fully populated MergedSettings object for testing purposes.
|
||||
* It merges the provided overrides with the default settings from the schema.
|
||||
*
|
||||
* @param overrides Partial settings to override the defaults.
|
||||
* @returns A complete MergedSettings object.
|
||||
*/
|
||||
export function createTestMergedSettings(
|
||||
overrides: Partial<Settings> = {},
|
||||
): MergedSettings {
|
||||
return customDeepMerge(
|
||||
getMergeStrategyForPath,
|
||||
getDefaultsFromSchema(),
|
||||
overrides,
|
||||
) as MergedSettings;
|
||||
}
|
||||
|
||||
export class LoadedSettings {
|
||||
@@ -264,14 +288,14 @@ export class LoadedSettings {
|
||||
readonly isTrusted: boolean;
|
||||
readonly errors: SettingsError[];
|
||||
|
||||
private _merged: Settings;
|
||||
private _merged: MergedSettings;
|
||||
private _remoteAdminSettings: Partial<Settings> | undefined;
|
||||
|
||||
get merged(): Settings {
|
||||
get merged(): MergedSettings {
|
||||
return this._merged;
|
||||
}
|
||||
|
||||
private computeMergedSettings(): Settings {
|
||||
private computeMergedSettings(): MergedSettings {
|
||||
const merged = mergeSettings(
|
||||
this.system.settings,
|
||||
this.systemDefaults.settings,
|
||||
@@ -293,7 +317,7 @@ export class LoadedSettings {
|
||||
(path: string[]) => getMergeStrategyForPath(['admin', ...path]),
|
||||
adminDefaults,
|
||||
this._remoteAdminSettings?.admin ?? {},
|
||||
) as Settings['admin'];
|
||||
) as MergedSettings['admin'];
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
BugCommandSettings,
|
||||
TelemetrySettings,
|
||||
AuthType,
|
||||
AgentOverride,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
@@ -799,7 +800,7 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Agent Overrides',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
default: {} as Record<string, AgentOverride>,
|
||||
description:
|
||||
'Override settings for specific agents, e.g. to disable the agent, set a custom model config, or run config.',
|
||||
showInDialog: false,
|
||||
@@ -2262,12 +2263,17 @@ type InferSettings<T extends SettingsSchema> = {
|
||||
: T[K]['default'];
|
||||
};
|
||||
|
||||
type InferMergedSettings<T extends SettingsSchema> = {
|
||||
-readonly [K in keyof T]-?: T[K] extends { properties: SettingsSchema }
|
||||
? InferMergedSettings<T[K]['properties']>
|
||||
: T[K]['type'] extends 'enum'
|
||||
? T[K]['options'] extends readonly SettingEnumOption[]
|
||||
? T[K]['options'][number]['value']
|
||||
: T[K]['default']
|
||||
: T[K]['default'] extends boolean
|
||||
? boolean
|
||||
: T[K]['default'];
|
||||
};
|
||||
|
||||
export type Settings = InferSettings<SettingsSchemaType>;
|
||||
|
||||
export function getEnableHooksUI(settings: Settings): boolean {
|
||||
return settings.tools?.enableHooks ?? true;
|
||||
}
|
||||
|
||||
export function getEnableHooks(settings: Settings): boolean {
|
||||
return getEnableHooksUI(settings) && (settings.hooks?.enabled ?? false);
|
||||
}
|
||||
export type MergedSettings = InferMergedSettings<SettingsSchemaType>;
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('initializer', () => {
|
||||
});
|
||||
|
||||
it('should handle undefined auth type', async () => {
|
||||
mockSettings.merged.security!.auth!.selectedType = undefined;
|
||||
mockSettings.merged.security.auth.selectedType = undefined;
|
||||
const result = await initializeApp(
|
||||
mockConfig as unknown as Config,
|
||||
mockSettings,
|
||||
|
||||
@@ -39,13 +39,13 @@ export async function initializeApp(
|
||||
const authHandle = startupProfiler.start('authenticate');
|
||||
const authError = await performInitialAuth(
|
||||
config,
|
||||
settings.merged.security?.auth?.selectedType,
|
||||
settings.merged.security.auth.selectedType,
|
||||
);
|
||||
authHandle?.end();
|
||||
const themeError = validateTheme(settings);
|
||||
|
||||
const shouldOpenAuthDialog =
|
||||
settings.merged.security?.auth?.selectedType === undefined || !!authError;
|
||||
settings.merged.security.auth.selectedType === undefined || !!authError;
|
||||
|
||||
logCliConfiguration(
|
||||
config,
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('theme', () => {
|
||||
});
|
||||
|
||||
it('should return null if theme is undefined', () => {
|
||||
mockSettings.merged.ui!.theme = undefined;
|
||||
mockSettings.merged.ui.theme = undefined;
|
||||
const result = validateTheme(mockSettings);
|
||||
expect(result).toBeNull();
|
||||
expect(themeManager.findThemeByName).not.toHaveBeenCalled();
|
||||
|
||||
@@ -13,7 +13,7 @@ import { type LoadedSettings } from '../config/settings.js';
|
||||
* @returns An error message if the theme is not found, otherwise null.
|
||||
*/
|
||||
export function validateTheme(settings: LoadedSettings): string | null {
|
||||
const effectiveTheme = settings.merged.ui?.theme;
|
||||
const effectiveTheme = settings.merged.ui.theme;
|
||||
if (effectiveTheme && !themeManager.findThemeByName(effectiveTheme)) {
|
||||
return `Theme "${effectiveTheme}" not found.`;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,30 @@ import {
|
||||
import os from 'node:os';
|
||||
import v8 from 'node:v8';
|
||||
import { type CliArgs } from './config/config.js';
|
||||
import { type LoadedSettings } from './config/settings.js';
|
||||
import {
|
||||
type LoadedSettings,
|
||||
type Settings,
|
||||
createTestMergedSettings,
|
||||
} from './config/settings.js';
|
||||
import { appEvents, AppEvent } from './utils/events.js';
|
||||
|
||||
function createMockSettings(
|
||||
overrides: Record<string, unknown> = {},
|
||||
): LoadedSettings {
|
||||
const merged = createTestMergedSettings(
|
||||
(overrides['merged'] as Partial<Settings>) || {},
|
||||
);
|
||||
|
||||
return {
|
||||
system: { settings: {} },
|
||||
systemDefaults: { settings: {} },
|
||||
user: { settings: {} },
|
||||
workspace: { settings: {} },
|
||||
errors: [],
|
||||
...overrides,
|
||||
merged,
|
||||
} as unknown as LoadedSettings;
|
||||
}
|
||||
import {
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
@@ -108,26 +130,19 @@ class MockProcessExitError extends Error {
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('./config/settings.js', () => ({
|
||||
loadSettings: vi.fn().mockReturnValue({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
ui: {},
|
||||
},
|
||||
vi.mock('./config/settings.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('./config/settings.js')>();
|
||||
return {
|
||||
...actual,
|
||||
loadSettings: vi.fn().mockImplementation(() => ({
|
||||
merged: actual.getDefaultsFromSchema(),
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
errors: [],
|
||||
}),
|
||||
migrateDeprecatedSettings: vi.fn(),
|
||||
SettingScope: {
|
||||
User: 'user',
|
||||
Workspace: 'workspace',
|
||||
System: 'system',
|
||||
SystemDefaults: 'system-defaults',
|
||||
},
|
||||
}));
|
||||
})),
|
||||
saveModelChange: vi.fn(),
|
||||
getDefaultsFromSchema: actual.getDefaultsFromSchema,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('./ui/utils/terminalCapabilityManager.js', () => ({
|
||||
terminalCapabilityManager: {
|
||||
@@ -443,17 +458,15 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
|
||||
},
|
||||
} as unknown as Config);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
errors: [],
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
ui: {},
|
||||
},
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
} as never);
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
model: undefined,
|
||||
sandbox: undefined,
|
||||
@@ -505,7 +518,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
@@ -514,8 +528,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
errors: [],
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
promptInteractive: false,
|
||||
@@ -594,7 +608,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
promptInteractive: false,
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
@@ -603,8 +618,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
errors: [],
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}),
|
||||
);
|
||||
|
||||
const mockConfig = {
|
||||
isInteractive: () => false,
|
||||
@@ -665,7 +680,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
@@ -674,8 +690,8 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
errors: [],
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
promptInteractive: false,
|
||||
@@ -753,13 +769,14 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
});
|
||||
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: { advanced: {}, security: { auth: {} }, ui: { theme: 'test' } },
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
errors: [],
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
promptInteractive: false,
|
||||
@@ -839,13 +856,14 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: { advanced: {}, security: { auth: {} }, ui: {} },
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
errors: [],
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
promptInteractive: false,
|
||||
@@ -918,13 +936,14 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: { advanced: {}, security: { auth: {} }, ui: {} },
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
errors: [],
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
promptInteractive: false,
|
||||
@@ -1034,10 +1053,11 @@ describe('gemini.tsx main function exit codes', () => {
|
||||
);
|
||||
const { loadSettings } = await import('./config/settings.js');
|
||||
vi.mocked(loadCliConfig).mockResolvedValue({} as Config);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: { security: { auth: {} }, ui: {} },
|
||||
errors: [],
|
||||
} as never);
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
promptInteractive: true,
|
||||
} as unknown as CliArgs);
|
||||
@@ -1066,14 +1086,13 @@ describe('gemini.tsx main function exit codes', () => {
|
||||
vi.mocked(loadCliConfig).mockResolvedValue({
|
||||
refreshAuth: vi.fn().mockRejectedValue(new Error('Auth failed')),
|
||||
} as unknown as Config);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: {
|
||||
security: { auth: { selectedType: 'google', useExternal: false } },
|
||||
ui: {},
|
||||
},
|
||||
workspace: { settings: {} },
|
||||
errors: [],
|
||||
} as never);
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({} as unknown as CliArgs);
|
||||
vi.mock('./config/auth.js', () => ({
|
||||
validateAuthMethod: vi.fn().mockReturnValue(null),
|
||||
@@ -1131,11 +1150,11 @@ describe('gemini.tsx main function exit codes', () => {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
|
||||
},
|
||||
} as unknown as Config);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: { security: { auth: {} }, ui: {} },
|
||||
workspace: { settings: {} },
|
||||
errors: [],
|
||||
} as never);
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
resume: 'invalid-session',
|
||||
} as unknown as CliArgs);
|
||||
@@ -1200,11 +1219,11 @@ describe('gemini.tsx main function exit codes', () => {
|
||||
},
|
||||
getRemoteAdminSettings: () => undefined,
|
||||
} as unknown as Config);
|
||||
vi.mocked(loadSettings).mockReturnValue({
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: { security: { auth: {} }, ui: {} },
|
||||
workspace: { settings: {} },
|
||||
errors: [],
|
||||
} as never);
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({} as unknown as CliArgs);
|
||||
Object.defineProperty(process.stdin, 'isTTY', {
|
||||
value: true, // Simulate TTY so it doesn't try to read stdin
|
||||
|
||||
+18
-19
@@ -213,12 +213,12 @@ export async function startInteractiveUI(
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<KeypressProvider
|
||||
config={config}
|
||||
debugKeystrokeLogging={settings.merged.general?.debugKeystrokeLogging}
|
||||
debugKeystrokeLogging={settings.merged.general.debugKeystrokeLogging}
|
||||
>
|
||||
<MouseProvider
|
||||
mouseEventsEnabled={mouseEventsEnabled}
|
||||
debugKeystrokeLogging={
|
||||
settings.merged.general?.debugKeystrokeLogging
|
||||
settings.merged.general.debugKeystrokeLogging
|
||||
}
|
||||
>
|
||||
<ScrollProvider>
|
||||
@@ -263,8 +263,7 @@ export async function startInteractiveUI(
|
||||
patchConsole: false,
|
||||
alternateBuffer: useAlternateBuffer,
|
||||
incrementalRendering:
|
||||
settings.merged.ui?.incrementalRendering !== false &&
|
||||
useAlternateBuffer,
|
||||
settings.merged.ui.incrementalRendering !== false && useAlternateBuffer,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -336,13 +335,13 @@ export async function main() {
|
||||
registerCleanup(consolePatcher.cleanup);
|
||||
|
||||
dns.setDefaultResultOrder(
|
||||
validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),
|
||||
validateDnsResolutionOrder(settings.merged.advanced.dnsResolutionOrder),
|
||||
);
|
||||
|
||||
// Set a default auth type if one isn't set or is set to a legacy type
|
||||
if (
|
||||
!settings.merged.security?.auth?.selectedType ||
|
||||
settings.merged.security?.auth?.selectedType === AuthType.LEGACY_CLOUD_SHELL
|
||||
!settings.merged.security.auth.selectedType ||
|
||||
settings.merged.security.auth.selectedType === AuthType.LEGACY_CLOUD_SHELL
|
||||
) {
|
||||
if (
|
||||
process.env['CLOUD_SHELL'] === 'true' ||
|
||||
@@ -364,8 +363,8 @@ export async function main() {
|
||||
// the sandbox because the sandbox will interfere with the Oauth2 web
|
||||
// redirect.
|
||||
if (
|
||||
settings.merged.security?.auth?.selectedType &&
|
||||
!settings.merged.security?.auth?.useExternal
|
||||
settings.merged.security.auth.selectedType &&
|
||||
!settings.merged.security.auth.useExternal
|
||||
) {
|
||||
try {
|
||||
if (partialConfig.isInteractive()) {
|
||||
@@ -381,8 +380,8 @@ export async function main() {
|
||||
);
|
||||
} else {
|
||||
const authType = await validateNonInteractiveAuth(
|
||||
settings.merged.security?.auth?.selectedType,
|
||||
settings.merged.security?.auth?.useExternal,
|
||||
settings.merged.security.auth.selectedType,
|
||||
settings.merged.security.auth.useExternal,
|
||||
partialConfig,
|
||||
settings,
|
||||
);
|
||||
@@ -403,7 +402,7 @@ export async function main() {
|
||||
|
||||
// hop into sandbox if we are outside and sandboxing is enabled
|
||||
if (!process.env['SANDBOX']) {
|
||||
const memoryArgs = settings.merged.advanced?.autoConfigureMemory
|
||||
const memoryArgs = settings.merged.advanced.autoConfigureMemory
|
||||
? getNodeMemoryArgs(isDebugMode)
|
||||
: [];
|
||||
const sandboxConfig = await loadSandboxConfig(settings.merged, argv);
|
||||
@@ -506,7 +505,7 @@ export async function main() {
|
||||
// Handle --list-sessions flag
|
||||
if (config.getListSessions()) {
|
||||
// Attempt auth for summary generation (gracefully skips if not configured)
|
||||
const authType = settings.merged.security?.auth?.selectedType;
|
||||
const authType = settings.merged.security.auth.selectedType;
|
||||
if (authType) {
|
||||
try {
|
||||
await config.refreshAuth(authType);
|
||||
@@ -566,7 +565,7 @@ export async function main() {
|
||||
initAppHandle?.end();
|
||||
|
||||
if (
|
||||
settings.merged.security?.auth?.selectedType ===
|
||||
settings.merged.security.auth.selectedType ===
|
||||
AuthType.LOGIN_WITH_GOOGLE &&
|
||||
config.isBrowserLaunchSuppressed()
|
||||
) {
|
||||
@@ -678,8 +677,8 @@ export async function main() {
|
||||
);
|
||||
|
||||
const authType = await validateNonInteractiveAuth(
|
||||
settings.merged.security?.auth?.selectedType,
|
||||
settings.merged.security?.auth?.useExternal,
|
||||
settings.merged.security.auth.selectedType,
|
||||
settings.merged.security.auth.useExternal,
|
||||
config,
|
||||
settings,
|
||||
);
|
||||
@@ -705,14 +704,14 @@ export async function main() {
|
||||
}
|
||||
|
||||
function setWindowTitle(title: string, settings: LoadedSettings) {
|
||||
if (!settings.merged.ui?.hideWindowTitle) {
|
||||
if (!settings.merged.ui.hideWindowTitle) {
|
||||
// Initial state before React loop starts
|
||||
const windowTitle = computeTerminalTitle({
|
||||
streamingState: StreamingState.Idle,
|
||||
isConfirming: false,
|
||||
folderName: title,
|
||||
showThoughts: !!settings.merged.ui?.showStatusInTitle,
|
||||
useDynamicTitle: settings.merged.ui?.dynamicWindowTitle ?? true,
|
||||
showThoughts: !!settings.merged.ui.showStatusInTitle,
|
||||
useDynamicTitle: settings.merged.ui.dynamicWindowTitle,
|
||||
});
|
||||
writeToStdout(`\x1b]0;${windowTitle}\x07`);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { vi } from 'vitest';
|
||||
import type { CommandContext } from '../ui/commands/types.js';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
import { mergeSettings } from '../config/settings.js';
|
||||
import type { GitService } from '@google/gemini-cli-core';
|
||||
import type { SessionStatsState } from '../ui/contexts/SessionContext.js';
|
||||
|
||||
@@ -27,6 +28,8 @@ type DeepPartial<T> = T extends object
|
||||
export const createMockCommandContext = (
|
||||
overrides: DeepPartial<CommandContext> = {},
|
||||
): CommandContext => {
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
|
||||
const defaultMocks: CommandContext = {
|
||||
invocation: {
|
||||
raw: '',
|
||||
@@ -35,7 +38,11 @@ export const createMockCommandContext = (
|
||||
},
|
||||
services: {
|
||||
config: null,
|
||||
settings: { merged: {} } as LoadedSettings,
|
||||
settings: {
|
||||
merged: defaultMergedSettings,
|
||||
setValue: vi.fn(),
|
||||
forScope: vi.fn().mockReturnValue({ settings: {} }),
|
||||
} as unknown as LoadedSettings,
|
||||
git: undefined as GitService | undefined,
|
||||
logger: {
|
||||
log: vi.fn(),
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('App', () => {
|
||||
pendingHistoryItems: [{ type: 'user', text: 'pending item' }],
|
||||
} as UIState;
|
||||
|
||||
mockLoadedSettings.merged.ui = { useAlternateBuffer: true };
|
||||
mockLoadedSettings.merged.ui.useAlternateBuffer = true;
|
||||
|
||||
const { lastFrame } = renderWithProviders(<App />, quittingUIState);
|
||||
|
||||
@@ -144,7 +144,7 @@ describe('App', () => {
|
||||
expect(lastFrame()).toContain('Quitting...');
|
||||
|
||||
// Reset settings
|
||||
mockLoadedSettings.merged.ui = { useAlternateBuffer: false };
|
||||
mockLoadedSettings.merged.ui.useAlternateBuffer = false;
|
||||
});
|
||||
|
||||
it('should render dialog manager when dialogs are visible', () => {
|
||||
|
||||
@@ -82,7 +82,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
import ansiEscapes from 'ansi-escapes';
|
||||
import type { LoadedSettings } from '../config/settings.js';
|
||||
import { type LoadedSettings, mergeSettings } from '../config/settings.js';
|
||||
import type { InitializationResult } from '../core/initializer.js';
|
||||
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
|
||||
import { UIStateContext, type UIState } from './contexts/UIStateContext.js';
|
||||
@@ -380,14 +380,17 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
|
||||
// Mock LoadedSettings
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
mockSettings = {
|
||||
merged: {
|
||||
...defaultMergedSettings,
|
||||
hideBanner: false,
|
||||
hideFooter: false,
|
||||
hideTips: false,
|
||||
showMemoryUsage: false,
|
||||
theme: 'default',
|
||||
ui: {
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: false,
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
@@ -507,8 +510,10 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
describe('Settings Integration', () => {
|
||||
it('handles settings with all display options disabled', async () => {
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const settingsAllHidden = {
|
||||
merged: {
|
||||
...defaultMergedSettings,
|
||||
hideBanner: true,
|
||||
hideFooter: true,
|
||||
hideTips: true,
|
||||
@@ -526,8 +531,10 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
it('handles settings with memory usage enabled', async () => {
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const settingsWithMemory = {
|
||||
merged: {
|
||||
...defaultMergedSettings,
|
||||
hideBanner: false,
|
||||
hideFooter: false,
|
||||
hideTips: false,
|
||||
@@ -574,7 +581,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
it('handles undefined settings gracefully', async () => {
|
||||
const undefinedSettings = {
|
||||
merged: {},
|
||||
merged: mergeSettings({}, {}, {}, {}, true),
|
||||
} as LoadedSettings;
|
||||
|
||||
let unmount: () => void;
|
||||
@@ -991,12 +998,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
it('should update terminal title with Working… when showStatusInTitle is false', () => {
|
||||
// Arrange: Set up mock settings with showStatusInTitle disabled
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const mockSettingsWithShowStatusFalse = {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
...defaultMergedSettings,
|
||||
ui: {
|
||||
...mockSettings.merged.ui,
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: false,
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
@@ -1073,12 +1081,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
it('should not update terminal title when hideWindowTitle is true', () => {
|
||||
// Arrange: Set up mock settings with hideWindowTitle enabled
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const mockSettingsWithHideTitleTrue = {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
...defaultMergedSettings,
|
||||
ui: {
|
||||
...mockSettings.merged.ui,
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: true,
|
||||
hideWindowTitle: true,
|
||||
},
|
||||
@@ -1101,12 +1110,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
it('should update terminal title with thought subject when in active state', () => {
|
||||
// Arrange: Set up mock settings with showStatusInTitle enabled
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const mockSettingsWithTitleEnabled = {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
...defaultMergedSettings,
|
||||
ui: {
|
||||
...mockSettings.merged.ui,
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: true,
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
@@ -1143,12 +1153,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
it('should update terminal title with default text when in Idle state and no thought subject', () => {
|
||||
// Arrange: Set up mock settings with showStatusInTitle enabled
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const mockSettingsWithTitleEnabled = {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
...defaultMergedSettings,
|
||||
ui: {
|
||||
...mockSettings.merged.ui,
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: true,
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
@@ -1184,12 +1195,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
it('should update terminal title when in WaitingForConfirmation state with thought subject', async () => {
|
||||
// Arrange: Set up mock settings with showStatusInTitle enabled
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const mockSettingsWithTitleEnabled = {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
...defaultMergedSettings,
|
||||
ui: {
|
||||
...mockSettings.merged.ui,
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: true,
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
@@ -1392,12 +1404,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
it('should pad title to exactly 80 characters', () => {
|
||||
// Arrange: Set up mock settings with showStatusInTitle enabled
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const mockSettingsWithTitleEnabled = {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
...defaultMergedSettings,
|
||||
ui: {
|
||||
...mockSettings.merged.ui,
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: true,
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
@@ -1435,12 +1448,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
it('should use correct ANSI escape code format', () => {
|
||||
// Arrange: Set up mock settings with showStatusInTitle enabled
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const mockSettingsWithTitleEnabled = {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
...defaultMergedSettings,
|
||||
ui: {
|
||||
...mockSettings.merged.ui,
|
||||
...defaultMergedSettings.ui,
|
||||
showStatusInTitle: true,
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
@@ -1802,12 +1816,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
const setupCopyModeTest = async (isAlternateMode = false) => {
|
||||
// Update settings for this test run
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const testSettings = {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
...defaultMergedSettings,
|
||||
ui: {
|
||||
...mockSettings.merged.ui,
|
||||
...defaultMergedSettings.ui,
|
||||
useAlternateBuffer: isAlternateMode,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -392,8 +392,8 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
}, []);
|
||||
|
||||
const getPreferredEditor = useCallback(
|
||||
() => settings.merged.general?.preferredEditor as EditorType,
|
||||
[settings.merged.general?.preferredEditor],
|
||||
() => settings.merged.general.preferredEditor as EditorType,
|
||||
[settings.merged.general.preferredEditor],
|
||||
);
|
||||
|
||||
const buffer = useTextBuffer({
|
||||
@@ -443,7 +443,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!(settings.merged.ui?.hideBanner || config.getScreenReader()) &&
|
||||
!(settings.merged.ui.hideBanner || config.getScreenReader()) &&
|
||||
bannerVisible &&
|
||||
bannerText
|
||||
) {
|
||||
@@ -603,17 +603,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
// Check for enforced auth type mismatch
|
||||
useEffect(() => {
|
||||
if (
|
||||
settings.merged.security?.auth?.enforcedType &&
|
||||
settings.merged.security?.auth.selectedType &&
|
||||
settings.merged.security?.auth.enforcedType !==
|
||||
settings.merged.security?.auth.selectedType
|
||||
settings.merged.security.auth.enforcedType &&
|
||||
settings.merged.security.auth.selectedType &&
|
||||
settings.merged.security.auth.enforcedType !==
|
||||
settings.merged.security.auth.selectedType
|
||||
) {
|
||||
onAuthError(
|
||||
`Authentication is enforced to be ${settings.merged.security?.auth.enforcedType}, but you are currently using ${settings.merged.security?.auth.selectedType}.`,
|
||||
`Authentication is enforced to be ${settings.merged.security.auth.enforcedType}, but you are currently using ${settings.merged.security.auth.selectedType}.`,
|
||||
);
|
||||
} else if (
|
||||
settings.merged.security?.auth?.selectedType &&
|
||||
!settings.merged.security?.auth?.useExternal
|
||||
settings.merged.security.auth.selectedType &&
|
||||
!settings.merged.security.auth.useExternal
|
||||
) {
|
||||
// We skip validation for Gemini API key here because it might be stored
|
||||
// in the keychain, which we can't check synchronously.
|
||||
@@ -630,9 +630,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
}
|
||||
}, [
|
||||
settings.merged.security?.auth?.selectedType,
|
||||
settings.merged.security?.auth?.enforcedType,
|
||||
settings.merged.security?.auth?.useExternal,
|
||||
settings.merged.security.auth.selectedType,
|
||||
settings.merged.security.auth.enforcedType,
|
||||
settings.merged.security.auth.useExternal,
|
||||
onAuthError,
|
||||
]);
|
||||
|
||||
@@ -951,8 +951,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING),
|
||||
1,
|
||||
),
|
||||
pager: settings.merged.tools?.shell?.pager,
|
||||
showColor: settings.merged.tools?.shell?.showColor,
|
||||
pager: settings.merged.tools.shell.pager,
|
||||
showColor: settings.merged.tools.shell.showColor,
|
||||
sanitizationConfig: config.sanitizationConfig,
|
||||
});
|
||||
|
||||
@@ -960,13 +960,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
// Context file names computation
|
||||
const contextFileNames = useMemo(() => {
|
||||
const fromSettings = settings.merged.context?.fileName;
|
||||
const fromSettings = settings.merged.context.fileName;
|
||||
return fromSettings
|
||||
? Array.isArray(fromSettings)
|
||||
? fromSettings
|
||||
: [fromSettings]
|
||||
: getAllGeminiMdFilenames();
|
||||
}, [settings.merged.context?.fileName]);
|
||||
}, [settings.merged.context.fileName]);
|
||||
// Initial prompt handling
|
||||
const initialPrompt = useMemo(() => config.getQuestion(), [config]);
|
||||
const initialPromptSubmitted = useRef(false);
|
||||
@@ -1040,7 +1040,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
const shouldShowIdePrompt = Boolean(
|
||||
currentIDE &&
|
||||
!config.getIdeMode() &&
|
||||
!settings.merged.ide?.hasSeenNudge &&
|
||||
!settings.merged.ide.hasSeenNudge &&
|
||||
!idePromptAnswered,
|
||||
);
|
||||
|
||||
@@ -1221,7 +1221,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(
|
||||
streamingState,
|
||||
settings.merged.ui?.customWittyPhrases,
|
||||
settings.merged.ui.customWittyPhrases,
|
||||
!!activePtyId && !embeddedShellFocused,
|
||||
lastOutputTime,
|
||||
retryStatus,
|
||||
@@ -1237,7 +1237,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
|
||||
// Debug log keystrokes if enabled
|
||||
if (settings.merged.general?.debugKeystrokeLogging) {
|
||||
if (settings.merged.general.debugKeystrokeLogging) {
|
||||
debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
|
||||
}
|
||||
|
||||
@@ -1337,7 +1337,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
cancelOngoingRequest,
|
||||
activePtyId,
|
||||
embeddedShellFocused,
|
||||
settings.merged.general?.debugKeystrokeLogging,
|
||||
settings.merged.general.debugKeystrokeLogging,
|
||||
refreshStatic,
|
||||
setCopyModeEnabled,
|
||||
copyModeEnabled,
|
||||
@@ -1351,7 +1351,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
// Update terminal title with Gemini CLI status and thoughts
|
||||
useEffect(() => {
|
||||
// Respect hideWindowTitle settings
|
||||
if (settings.merged.ui?.hideWindowTitle) return;
|
||||
if (settings.merged.ui.hideWindowTitle) return;
|
||||
|
||||
const paddedTitle = computeTerminalTitle({
|
||||
streamingState,
|
||||
@@ -1361,8 +1361,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
!!confirmationRequest ||
|
||||
showShellActionRequired,
|
||||
folderName: basename(config.getTargetDir()),
|
||||
showThoughts: !!settings.merged.ui?.showStatusInTitle,
|
||||
useDynamicTitle: settings.merged.ui?.dynamicWindowTitle ?? true,
|
||||
showThoughts: !!settings.merged.ui.showStatusInTitle,
|
||||
useDynamicTitle: settings.merged.ui.dynamicWindowTitle,
|
||||
});
|
||||
|
||||
// Only update the title if it's different from the last value we set
|
||||
@@ -1377,9 +1377,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
shellConfirmationRequest,
|
||||
confirmationRequest,
|
||||
showShellActionRequired,
|
||||
settings.merged.ui?.showStatusInTitle,
|
||||
settings.merged.ui?.dynamicWindowTitle,
|
||||
settings.merged.ui?.hideWindowTitle,
|
||||
settings.merged.ui.showStatusInTitle,
|
||||
settings.merged.ui.dynamicWindowTitle,
|
||||
settings.merged.ui.hideWindowTitle,
|
||||
config,
|
||||
stdout,
|
||||
]);
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('filters auth types when enforcedType is set', () => {
|
||||
props.settings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
expect(items).toHaveLength(1);
|
||||
@@ -160,7 +160,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('sets initial index to 0 when enforcedType is set', () => {
|
||||
props.settings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
expect(initialIndex).toBe(0);
|
||||
@@ -170,7 +170,7 @@ describe('AuthDialog', () => {
|
||||
it.each([
|
||||
{
|
||||
setup: () => {
|
||||
props.settings.merged.security!.auth!.selectedType =
|
||||
props.settings.merged.security.auth.selectedType =
|
||||
AuthType.USE_VERTEX_AI;
|
||||
},
|
||||
expected: AuthType.USE_VERTEX_AI,
|
||||
@@ -290,7 +290,7 @@ describe('AuthDialog', () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
process.env['GEMINI_API_KEY'] = 'test-key-from-env';
|
||||
// Simulate that the user has already authenticated once
|
||||
props.settings.merged.security!.auth!.selectedType =
|
||||
props.settings.merged.security.auth.selectedType =
|
||||
AuthType.LOGIN_WITH_GOOGLE;
|
||||
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
@@ -349,7 +349,7 @@ describe('AuthDialog', () => {
|
||||
{
|
||||
desc: 'calls onAuthError on escape if no auth method is set',
|
||||
setup: () => {
|
||||
props.settings.merged.security!.auth!.selectedType = undefined;
|
||||
props.settings.merged.security.auth.selectedType = undefined;
|
||||
},
|
||||
expectations: (p: typeof props) => {
|
||||
expect(p.onAuthError).toHaveBeenCalledWith(
|
||||
@@ -360,7 +360,7 @@ describe('AuthDialog', () => {
|
||||
{
|
||||
desc: 'calls setAuthState(Unauthenticated) on escape if auth method is set',
|
||||
setup: () => {
|
||||
props.settings.merged.security!.auth!.selectedType =
|
||||
props.settings.merged.security.auth.selectedType =
|
||||
AuthType.USE_GEMINI;
|
||||
},
|
||||
expectations: (p: typeof props) => {
|
||||
@@ -392,7 +392,7 @@ describe('AuthDialog', () => {
|
||||
});
|
||||
|
||||
it('renders correctly with enforced auth type', () => {
|
||||
props.settings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -78,9 +78,9 @@ export function AuthDialog({
|
||||
},
|
||||
];
|
||||
|
||||
if (settings.merged.security?.auth?.enforcedType) {
|
||||
if (settings.merged.security.auth.enforcedType) {
|
||||
items = items.filter(
|
||||
(item) => item.value === settings.merged.security?.auth?.enforcedType,
|
||||
(item) => item.value === settings.merged.security.auth.enforcedType,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export function AuthDialog({
|
||||
}
|
||||
|
||||
let initialAuthIndex = items.findIndex((item) => {
|
||||
if (settings.merged.security?.auth?.selectedType) {
|
||||
if (settings.merged.security.auth.selectedType) {
|
||||
return item.value === settings.merged.security.auth.selectedType;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export function AuthDialog({
|
||||
|
||||
return item.value === AuthType.LOGIN_WITH_GOOGLE;
|
||||
});
|
||||
if (settings.merged.security?.auth?.enforcedType) {
|
||||
if (settings.merged.security.auth.enforcedType) {
|
||||
initialAuthIndex = 0;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ export function AuthDialog({
|
||||
if (authError) {
|
||||
return;
|
||||
}
|
||||
if (settings.merged.security?.auth?.selectedType === undefined) {
|
||||
if (settings.merged.security.auth.selectedType === undefined) {
|
||||
// Prevent exiting if no auth method is set
|
||||
onAuthError(
|
||||
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
||||
|
||||
@@ -20,11 +20,11 @@ export function validateAuthMethodWithSettings(
|
||||
authType: AuthType,
|
||||
settings: LoadedSettings,
|
||||
): string | null {
|
||||
const enforcedType = settings.merged.security?.auth?.enforcedType;
|
||||
const enforcedType = settings.merged.security.auth.enforcedType;
|
||||
if (enforcedType && enforcedType !== authType) {
|
||||
return `Authentication is enforced to be ${enforcedType}, but you are currently using ${authType}.`;
|
||||
}
|
||||
if (settings.merged.security?.auth?.useExternal) {
|
||||
if (settings.merged.security.auth.useExternal) {
|
||||
return null;
|
||||
}
|
||||
// If using Gemini API key, we don't validate it here as we might need to prompt for it.
|
||||
@@ -80,7 +80,7 @@ export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const authType = settings.merged.security?.auth?.selectedType;
|
||||
const authType = settings.merged.security.auth.selectedType;
|
||||
if (!authType) {
|
||||
if (process.env['GEMINI_API_KEY']) {
|
||||
onAuthError(
|
||||
|
||||
@@ -33,7 +33,7 @@ export const aboutCommand: SlashCommand = {
|
||||
const modelVersion = context.services.config?.getModel() || 'Unknown';
|
||||
const cliVersion = await getVersion();
|
||||
const selectedAuthType =
|
||||
context.services.settings.merged.security?.auth?.selectedType || '';
|
||||
context.services.settings.merged.security.auth.selectedType || '';
|
||||
const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || '';
|
||||
const ideClient = await getIdeClientName(context);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { agentsCommand } from './agentsCommand.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import type { Config, AgentOverride } from '@google/gemini-cli-core';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { enableAgent, disableAgent } from '../../utils/agentSettings.js';
|
||||
@@ -148,12 +148,9 @@ describe('agentsCommand', () => {
|
||||
reload: reloadSpy,
|
||||
});
|
||||
// Add agent to disabled overrides so validation passes
|
||||
(
|
||||
mockContext.services.settings.merged.agents!.overrides as Record<
|
||||
string,
|
||||
AgentOverride
|
||||
>
|
||||
)['test-agent'] = { disabled: true };
|
||||
mockContext.services.settings.merged.agents.overrides['test-agent'] = {
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
vi.mocked(enableAgent).mockReturnValue({
|
||||
status: 'success',
|
||||
@@ -266,12 +263,9 @@ describe('agentsCommand', () => {
|
||||
|
||||
it('should show info message if agent is already disabled', async () => {
|
||||
mockConfig.getAgentRegistry().getAllAgentNames.mockReturnValue([]);
|
||||
(
|
||||
mockContext.services.settings.merged.agents!.overrides as Record<
|
||||
string,
|
||||
AgentOverride
|
||||
>
|
||||
)['test-agent'] = { disabled: true };
|
||||
mockContext.services.settings.merged.agents.overrides['test-agent'] = {
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
const disableCommand = agentsCommand.subCommands?.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
import { CommandKind } from './types.js';
|
||||
import { MessageType, type HistoryItemAgentsList } from '../types.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import type { AgentOverride } from '@google/gemini-cli-core';
|
||||
import { disableAgent, enableAgent } from '../../utils/agentSettings.js';
|
||||
import { renderAgentActionFeedback } from '../../utils/agentUtils.js';
|
||||
|
||||
@@ -84,10 +83,7 @@ async function enableAction(
|
||||
}
|
||||
|
||||
const allAgents = agentRegistry.getAllAgentNames();
|
||||
const overrides = (settings.merged.agents?.overrides ?? {}) as Record<
|
||||
string,
|
||||
AgentOverride
|
||||
>;
|
||||
const overrides = settings.merged.agents.overrides;
|
||||
const disabledAgents = Object.keys(overrides).filter(
|
||||
(name) => overrides[name]?.disabled === true,
|
||||
);
|
||||
@@ -157,10 +153,7 @@ async function disableAction(
|
||||
}
|
||||
|
||||
const allAgents = agentRegistry.getAllAgentNames();
|
||||
const overrides = (settings.merged.agents?.overrides ?? {}) as Record<
|
||||
string,
|
||||
AgentOverride
|
||||
>;
|
||||
const overrides = settings.merged.agents.overrides;
|
||||
const disabledAgents = Object.keys(overrides).filter(
|
||||
(name) => overrides[name]?.disabled === true,
|
||||
);
|
||||
@@ -211,10 +204,7 @@ function completeAgentsToEnable(context: CommandContext, partialArg: string) {
|
||||
const { config, settings } = context.services;
|
||||
if (!config) return [];
|
||||
|
||||
const overrides = (settings.merged.agents?.overrides ?? {}) as Record<
|
||||
string,
|
||||
AgentOverride
|
||||
>;
|
||||
const overrides = settings.merged.agents.overrides;
|
||||
const disabledAgents = Object.entries(overrides)
|
||||
.filter(([_, override]) => override?.disabled === true)
|
||||
.map(([name]) => name);
|
||||
|
||||
@@ -271,9 +271,10 @@ describe('hooksCommand', () => {
|
||||
|
||||
it('should enable a hook and update settings', async () => {
|
||||
// Update the context's settings with disabled hooks
|
||||
mockContext.services.settings.merged.hooks = {
|
||||
disabled: ['test-hook', 'other-hook'],
|
||||
};
|
||||
mockContext.services.settings.merged.hooks.disabled = [
|
||||
'test-hook',
|
||||
'other-hook',
|
||||
];
|
||||
|
||||
const enableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'enable',
|
||||
@@ -401,9 +402,7 @@ describe('hooksCommand', () => {
|
||||
});
|
||||
|
||||
it('should disable a hook and update settings', async () => {
|
||||
mockContext.services.settings.merged.hooks = {
|
||||
disabled: [],
|
||||
};
|
||||
mockContext.services.settings.merged.hooks.disabled = [];
|
||||
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
@@ -432,9 +431,7 @@ describe('hooksCommand', () => {
|
||||
|
||||
it('should return info when hook is already disabled', async () => {
|
||||
// Update the context's settings with the hook already disabled
|
||||
mockContext.services.settings.merged.hooks = {
|
||||
disabled: ['test-hook'],
|
||||
};
|
||||
mockContext.services.settings.merged.hooks.disabled = ['test-hook'];
|
||||
|
||||
const disableCmd = hooksCommand.subCommands!.find(
|
||||
(cmd) => cmd.name === 'disable',
|
||||
@@ -455,9 +452,7 @@ describe('hooksCommand', () => {
|
||||
});
|
||||
|
||||
it('should handle error when disabling hook fails', async () => {
|
||||
mockContext.services.settings.merged.hooks = {
|
||||
disabled: [],
|
||||
};
|
||||
mockContext.services.settings.merged.hooks.disabled = [];
|
||||
mockSettings.setValue.mockImplementationOnce(() => {
|
||||
throw new Error('Failed to save settings');
|
||||
});
|
||||
|
||||
@@ -76,8 +76,7 @@ async function enableAction(
|
||||
|
||||
// Get current disabled hooks from settings
|
||||
const settings = context.services.settings;
|
||||
const disabledHooks = settings.merged.hooks?.disabled || ([] as string[]);
|
||||
|
||||
const disabledHooks = settings.merged.hooks.disabled;
|
||||
// Remove from disabled list if present
|
||||
const newDisabledHooks = disabledHooks.filter(
|
||||
(name: string) => name !== hookName,
|
||||
@@ -143,8 +142,7 @@ async function disableAction(
|
||||
|
||||
// Get current disabled hooks from settings
|
||||
const settings = context.services.settings;
|
||||
const disabledHooks = settings.merged.hooks?.disabled || ([] as string[]);
|
||||
|
||||
const disabledHooks = settings.merged.hooks.disabled;
|
||||
// Add to disabled list if not already present
|
||||
if (!disabledHooks.includes(hookName)) {
|
||||
const newDisabledHooks = [...disabledHooks, hookName];
|
||||
|
||||
@@ -26,7 +26,7 @@ export const AppHeader = ({ version }: AppHeaderProps) => {
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{!(settings.merged.ui?.hideBanner || config.getScreenReader()) && (
|
||||
{!(settings.merged.ui.hideBanner || config.getScreenReader()) && (
|
||||
<>
|
||||
<Header version={version} nightly={nightly} />
|
||||
{bannerVisible && bannerText && (
|
||||
@@ -38,7 +38,7 @@ export const AppHeader = ({ version }: AppHeaderProps) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!(settings.merged.ui?.hideTips || config.getScreenReader()) && (
|
||||
{!(settings.merged.ui.hideTips || config.getScreenReader()) && (
|
||||
<Tips config={config} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -24,6 +24,7 @@ vi.mock('../contexts/VimModeContext.js', () => ({
|
||||
}));
|
||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { mergeSettings } from '../../config/settings.js';
|
||||
|
||||
// Mock child components
|
||||
vi.mock('./LoadingIndicator.js', () => ({
|
||||
@@ -163,13 +164,20 @@ const createMockConfig = (overrides = {}) => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockSettings = (merged = {}) => ({
|
||||
const createMockSettings = (merged = {}) => {
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
return {
|
||||
merged: {
|
||||
...defaultMergedSettings,
|
||||
ui: {
|
||||
...defaultMergedSettings.ui,
|
||||
hideFooter: false,
|
||||
showMemoryUsage: false,
|
||||
...merged,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const renderComposer = (
|
||||
|
||||
@@ -82,9 +82,7 @@ export const Composer = () => {
|
||||
<Box
|
||||
marginTop={1}
|
||||
justifyContent={
|
||||
settings.merged.ui?.hideContextSummary
|
||||
? 'flex-start'
|
||||
: 'space-between'
|
||||
settings.merged.ui.hideContextSummary ? 'flex-start' : 'space-between'
|
||||
}
|
||||
width="100%"
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
@@ -153,7 +151,7 @@ export const Composer = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!settings.merged.ui?.hideFooter && !isScreenReaderEnabled && <Footer />}
|
||||
{!settings.merged.ui.hideFooter && !isScreenReaderEnabled && <Footer />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -124,12 +124,12 @@ export function EditorSettingsDialog({
|
||||
|
||||
let mergedEditorName = 'None';
|
||||
if (
|
||||
settings.merged.general?.preferredEditor &&
|
||||
isEditorAvailable(settings.merged.general?.preferredEditor)
|
||||
settings.merged.general.preferredEditor &&
|
||||
isEditorAvailable(settings.merged.general.preferredEditor)
|
||||
) {
|
||||
mergedEditorName =
|
||||
EDITOR_DISPLAY_NAMES[
|
||||
settings.merged.general?.preferredEditor as EditorType
|
||||
settings.merged.general.preferredEditor as EditorType
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -59,12 +59,11 @@ export const Footer: React.FC = () => {
|
||||
};
|
||||
|
||||
const showMemoryUsage =
|
||||
config.getDebugMode() || settings.merged.ui?.showMemoryUsage || false;
|
||||
const hideCWD = settings.merged.ui?.footer?.hideCWD;
|
||||
const hideSandboxStatus = settings.merged.ui?.footer?.hideSandboxStatus;
|
||||
const hideModelInfo = settings.merged.ui?.footer?.hideModelInfo;
|
||||
const hideContextPercentage =
|
||||
settings.merged.ui?.footer?.hideContextPercentage;
|
||||
config.getDebugMode() || settings.merged.ui.showMemoryUsage;
|
||||
const hideCWD = settings.merged.ui.footer.hideCWD;
|
||||
const hideSandboxStatus = settings.merged.ui.footer.hideSandboxStatus;
|
||||
const hideModelInfo = settings.merged.ui.footer.hideModelInfo;
|
||||
const hideContextPercentage = settings.merged.ui.footer.hideContextPercentage;
|
||||
|
||||
const pathLength = Math.max(20, Math.floor(mainAreaWidth * 0.25));
|
||||
const displayPath = shortenPath(tildeifyPath(targetDir), pathLength);
|
||||
|
||||
@@ -52,14 +52,11 @@ export const StatusDisplay: React.FC<StatusDisplayProps> = ({
|
||||
return <Text color={theme.status.error}>{uiState.queueErrorMessage}</Text>;
|
||||
}
|
||||
|
||||
if (
|
||||
uiState.activeHooks.length > 0 &&
|
||||
(settings.merged.hooks?.notifications ?? true)
|
||||
) {
|
||||
if (uiState.activeHooks.length > 0 && settings.merged.hooks.notifications) {
|
||||
return <HookStatusDisplay activeHooks={uiState.activeHooks} />;
|
||||
}
|
||||
|
||||
if (!settings.merged.ui?.hideContextSummary && !hideContextSummary) {
|
||||
if (!settings.merged.ui.hideContextSummary && !hideContextSummary) {
|
||||
return (
|
||||
<ContextSummaryDisplay
|
||||
ideContext={uiState.ideContextState}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function ThemeDialog({
|
||||
const [highlightedThemeName, setHighlightedThemeName] = useState<string>(
|
||||
() => {
|
||||
// If a theme is already set, use it.
|
||||
if (settings.merged.ui?.theme) {
|
||||
if (settings.merged.ui.theme) {
|
||||
return settings.merged.ui.theme;
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export function ThemeDialog({
|
||||
const customThemes =
|
||||
selectedScope === SettingScope.User
|
||||
? settings.user.settings.ui?.customThemes || {}
|
||||
: settings.merged.ui?.customThemes || {};
|
||||
: settings.merged.ui.customThemes;
|
||||
const builtInThemes = themeManager
|
||||
.getAvailableThemes()
|
||||
.filter((theme) => theme.type !== 'custom');
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
|
||||
const settings = useSettings();
|
||||
const allowPermanentApproval =
|
||||
settings.merged.security?.enablePermanentToolApproval ?? false;
|
||||
settings.merged.security.enablePermanentToolApproval;
|
||||
|
||||
const [ideClient, setIdeClient] = useState<IdeClient | null>(null);
|
||||
const [isDiffingEnabled, setIsDiffingEnabled] = useState(false);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const VimModeProvider = ({
|
||||
children: React.ReactNode;
|
||||
settings: LoadedSettings;
|
||||
}) => {
|
||||
const initialVimEnabled = settings.merged.general?.vimMode ?? false;
|
||||
const initialVimEnabled = settings.merged.general.vimMode;
|
||||
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.general?.vimMode ?? false;
|
||||
const enabled = settings.merged.general.vimMode;
|
||||
setVimEnabled(enabled);
|
||||
// When vim mode is enabled, always start in NORMAL mode
|
||||
if (enabled) {
|
||||
setVimMode('NORMAL');
|
||||
}
|
||||
}, [settings.merged.general?.vimMode]);
|
||||
}, [settings.merged.general.vimMode]);
|
||||
|
||||
const toggleVimEnabled = useCallback(async () => {
|
||||
const newValue = !vimEnabled;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
export const isAlternateBufferEnabled = (settings: LoadedSettings): boolean =>
|
||||
settings.merged.ui?.useAlternateBuffer === true;
|
||||
settings.merged.ui.useAlternateBuffer === true;
|
||||
|
||||
export const useAlternateBuffer = (): boolean => {
|
||||
const settings = useSettings();
|
||||
|
||||
@@ -27,7 +27,7 @@ export const useFolderTrust = (
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
const startupMessageSent = useRef(false);
|
||||
|
||||
const folderTrust = settings.merged.security?.folderTrust?.enabled;
|
||||
const folderTrust = settings.merged.security.folderTrust.enabled;
|
||||
|
||||
useEffect(() => {
|
||||
const { isTrusted: trusted } = isWorkspaceTrusted(settings.merged);
|
||||
|
||||
@@ -83,7 +83,7 @@ enum StreamProcessingStatus {
|
||||
}
|
||||
|
||||
function showCitations(settings: LoadedSettings): boolean {
|
||||
const enabled = settings?.merged?.ui?.showCitations;
|
||||
const enabled = settings.merged.ui.showCitations;
|
||||
if (enabled !== undefined) {
|
||||
return enabled;
|
||||
}
|
||||
@@ -782,7 +782,7 @@ export const useGeminiStream = (
|
||||
|
||||
const handleChatModelEvent = useCallback(
|
||||
(eventValue: string, userMessageTimestamp: number) => {
|
||||
if (!settings?.merged?.ui?.showModelInfoInChat) {
|
||||
if (!settings.merged.ui.showModelInfoInChat) {
|
||||
return;
|
||||
}
|
||||
if (pendingHistoryItemRef.current) {
|
||||
|
||||
@@ -85,7 +85,7 @@ export const usePermissionsModifyTrust = (
|
||||
);
|
||||
const [needsRestart, setNeedsRestart] = useState(false);
|
||||
|
||||
const isFolderTrustEnabled = !!settings.merged.security?.folderTrust?.enabled;
|
||||
const isFolderTrustEnabled = !!settings.merged.security.folderTrust.enabled;
|
||||
|
||||
const updateTrustLevel = useCallback(
|
||||
(trustLevel: TrustLevel) => {
|
||||
|
||||
@@ -32,7 +32,7 @@ export function createShowMemoryAction(
|
||||
|
||||
const currentMemory = config.getUserMemory();
|
||||
const fileCount = config.getGeminiMdFileCount();
|
||||
const contextFileName = settings.merged.context?.fileName;
|
||||
const contextFileName = settings.merged.context.fileName;
|
||||
const contextFileNames = Array.isArray(contextFileName)
|
||||
? contextFileName
|
||||
: [contextFileName];
|
||||
|
||||
@@ -67,7 +67,7 @@ export const useThemeCommand = (
|
||||
|
||||
const closeThemeDialog = useCallback(() => {
|
||||
// Re-apply the saved theme to revert any preview changes from highlighting
|
||||
applyTheme(loadedSettings.merged.ui?.theme);
|
||||
applyTheme(loadedSettings.merged.ui.theme);
|
||||
setIsThemeDialogOpen(false);
|
||||
}, [applyTheme, loadedSettings]);
|
||||
|
||||
@@ -88,10 +88,10 @@ export const useThemeCommand = (
|
||||
return;
|
||||
}
|
||||
loadedSettings.setValue(scope, 'ui.theme', themeName); // Update the merged settings
|
||||
if (loadedSettings.merged.ui?.customThemes) {
|
||||
themeManager.loadCustomThemes(loadedSettings.merged.ui?.customThemes);
|
||||
if (loadedSettings.merged.ui.customThemes) {
|
||||
themeManager.loadCustomThemes(loadedSettings.merged.ui.customThemes);
|
||||
}
|
||||
applyTheme(loadedSettings.merged.ui?.theme); // Apply the current theme
|
||||
applyTheme(loadedSettings.merged.ui.theme); // Apply the current theme
|
||||
setThemeError(null);
|
||||
} finally {
|
||||
setIsThemeDialogOpen(false); // Close the dialog
|
||||
|
||||
@@ -149,7 +149,7 @@ export function colorizeCode({
|
||||
const activeTheme = theme || themeManager.getActiveTheme();
|
||||
const showLineNumbers = hideLineNumbers
|
||||
? false
|
||||
: (settings?.merged.ui?.showLineNumbers ?? true);
|
||||
: settings.merged.ui.showLineNumbers;
|
||||
|
||||
const useMaxSizedBox = !isAlternateBufferEnabled(settings);
|
||||
try {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const calculateMainAreaWidth = (
|
||||
terminalWidth: number,
|
||||
settings: LoadedSettings,
|
||||
): number => {
|
||||
if (settings.merged.ui?.useFullWidth) {
|
||||
if (settings.merged.ui.useFullWidth) {
|
||||
if (isAlternateBufferEnabled(settings)) {
|
||||
return terminalWidth - 1;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('checkForUpdates', () => {
|
||||
});
|
||||
|
||||
it('should return null if disableUpdateNag is true', async () => {
|
||||
mockSettings.merged.general!.disableUpdateNag = true;
|
||||
mockSettings.merged.general.disableUpdateNag = true;
|
||||
const result = await checkForUpdates(mockSettings);
|
||||
expect(result).toBeNull();
|
||||
expect(getPackageJson).not.toHaveBeenCalled();
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function checkForUpdates(
|
||||
settings: LoadedSettings,
|
||||
): Promise<UpdateObject | null> {
|
||||
try {
|
||||
if (settings.merged.general?.disableUpdateNag) {
|
||||
if (settings.merged.general.disableUpdateNag) {
|
||||
return null;
|
||||
}
|
||||
// Skip update check when running from source (development mode)
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
type LoadedSettings,
|
||||
} from '../config/settings.js';
|
||||
import type { ModifiedScope } from './skillSettings.js';
|
||||
import type { AgentOverride } from '@google/gemini-cli-core';
|
||||
|
||||
export type AgentActionStatus = 'success' | 'no-op' | 'error';
|
||||
|
||||
@@ -44,8 +43,8 @@ export function enableAgent(
|
||||
for (const scope of writableScopes) {
|
||||
if (isLoadableSettingScope(scope)) {
|
||||
const scopePath = settings.forScope(scope).path;
|
||||
const agentOverrides = settings.forScope(scope).settings.agents
|
||||
?.overrides as Record<string, AgentOverride> | undefined;
|
||||
const agentOverrides =
|
||||
settings.forScope(scope).settings.agents?.overrides;
|
||||
const isDisabled = agentOverrides?.[agentName]?.disabled === true;
|
||||
|
||||
if (isDisabled) {
|
||||
@@ -105,9 +104,7 @@ export function disableAgent(
|
||||
}
|
||||
|
||||
const scopePath = settings.forScope(scope).path;
|
||||
const agentOverrides = settings.forScope(scope).settings.agents?.overrides as
|
||||
| Record<string, AgentOverride>
|
||||
| undefined;
|
||||
const agentOverrides = settings.forScope(scope).settings.agents?.overrides;
|
||||
const isDisabled = agentOverrides?.[agentName]?.disabled === true;
|
||||
|
||||
if (isDisabled) {
|
||||
@@ -128,8 +125,8 @@ export function disableAgent(
|
||||
const alreadyDisabledInOther: ModifiedScope[] = [];
|
||||
|
||||
if (isLoadableSettingScope(otherScope)) {
|
||||
const otherOverrides = settings.forScope(otherScope).settings.agents
|
||||
?.overrides as Record<string, AgentOverride> | undefined;
|
||||
const otherOverrides =
|
||||
settings.forScope(otherScope).settings.agents?.overrides;
|
||||
if (otherOverrides?.[agentName]?.disabled === true) {
|
||||
alreadyDisabledInOther.push({
|
||||
scope: otherScope,
|
||||
|
||||
@@ -14,6 +14,7 @@ import EventEmitter from 'node:events';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { handleAutoUpdate, setUpdateHandler } from './handleAutoUpdate.js';
|
||||
import { MessageType } from '../ui/types.js';
|
||||
import { mergeSettings } from '../config/settings.js';
|
||||
|
||||
vi.mock('./installationInfo.js', async () => {
|
||||
const actual = await vi.importActual('./installationInfo.js');
|
||||
@@ -49,12 +50,9 @@ describe('handleAutoUpdate', () => {
|
||||
message: 'An update is available!',
|
||||
};
|
||||
|
||||
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
mockSettings = {
|
||||
merged: {
|
||||
general: {
|
||||
disableAutoUpdate: false,
|
||||
},
|
||||
},
|
||||
merged: defaultMergedSettings,
|
||||
} as LoadedSettings;
|
||||
|
||||
mockChildProcess = Object.assign(new EventEmitter(), {
|
||||
@@ -82,7 +80,7 @@ describe('handleAutoUpdate', () => {
|
||||
});
|
||||
|
||||
it('should do nothing if update nag is disabled', () => {
|
||||
mockSettings.merged.general!.disableUpdateNag = true;
|
||||
mockSettings.merged.general.disableUpdateNag = true;
|
||||
handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);
|
||||
expect(mockGetInstallationInfo).not.toHaveBeenCalled();
|
||||
expect(updateEventEmitter.emit).not.toHaveBeenCalled();
|
||||
@@ -90,7 +88,7 @@ describe('handleAutoUpdate', () => {
|
||||
});
|
||||
|
||||
it('should emit "update-received" but not update if auto-updates are disabled', () => {
|
||||
mockSettings.merged.general!.disableAutoUpdate = true;
|
||||
mockSettings.merged.general.disableAutoUpdate = true;
|
||||
mockGetInstallationInfo.mockReturnValue({
|
||||
updateCommand: 'npm i -g @google/gemini-cli@latest',
|
||||
updateMessage: 'Please update manually.',
|
||||
|
||||
@@ -23,20 +23,20 @@ export function handleAutoUpdate(
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.merged.tools?.sandbox || process.env['GEMINI_SANDBOX']) {
|
||||
if (settings.merged.tools.sandbox || process.env['GEMINI_SANDBOX']) {
|
||||
updateEventEmitter.emit('update-info', {
|
||||
message: `${info.message}\nAutomatic update is not available in sandbox mode.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.merged.general?.disableUpdateNag) {
|
||||
if (settings.merged.general.disableUpdateNag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installationInfo = getInstallationInfo(
|
||||
projectRoot,
|
||||
settings.merged.general?.disableAutoUpdate ?? false,
|
||||
settings.merged.general.disableAutoUpdate,
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -58,7 +58,7 @@ export function handleAutoUpdate(
|
||||
|
||||
if (
|
||||
!installationInfo.updateCommand ||
|
||||
settings.merged.general?.disableAutoUpdate
|
||||
settings.merged.general.disableAutoUpdate
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,14 +32,14 @@ export async function setupTerminalAndTheme(
|
||||
}
|
||||
|
||||
// Load custom themes from settings
|
||||
themeManager.loadCustomThemes(settings.merged.ui?.customThemes);
|
||||
themeManager.loadCustomThemes(settings.merged.ui.customThemes);
|
||||
|
||||
if (settings.merged.ui?.theme) {
|
||||
if (!themeManager.setActiveTheme(settings.merged.ui?.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 AppContainer.tsx will handle opening the dialog.
|
||||
debugLogger.warn(
|
||||
`Warning: Theme "${settings.merged.ui?.theme}" not found.`,
|
||||
`Warning: Theme "${settings.merged.ui.theme}" not found.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -295,7 +295,7 @@ describe('validateNonInterActiveAuth', () => {
|
||||
});
|
||||
|
||||
it('succeeds if effectiveAuthType matches enforcedAuthType', async () => {
|
||||
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
|
||||
mockSettings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
process.env['GEMINI_API_KEY'] = 'fake-key';
|
||||
const nonInteractiveConfig = createLocalMockConfig({});
|
||||
await validateNonInteractiveAuth(
|
||||
@@ -309,8 +309,7 @@ describe('validateNonInterActiveAuth', () => {
|
||||
});
|
||||
|
||||
it('exits if configuredAuthType does not match enforcedAuthType', async () => {
|
||||
mockSettings.merged.security!.auth!.enforcedType =
|
||||
AuthType.LOGIN_WITH_GOOGLE;
|
||||
mockSettings.merged.security.auth.enforcedType = AuthType.LOGIN_WITH_GOOGLE;
|
||||
const nonInteractiveConfig = createLocalMockConfig({
|
||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
|
||||
});
|
||||
@@ -336,8 +335,7 @@ describe('validateNonInterActiveAuth', () => {
|
||||
});
|
||||
|
||||
it('exits if auth from env var does not match enforcedAuthType', async () => {
|
||||
mockSettings.merged.security!.auth!.enforcedType =
|
||||
AuthType.LOGIN_WITH_GOOGLE;
|
||||
mockSettings.merged.security.auth.enforcedType = AuthType.LOGIN_WITH_GOOGLE;
|
||||
process.env['GEMINI_API_KEY'] = 'fake-key';
|
||||
const nonInteractiveConfig = createLocalMockConfig({
|
||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
|
||||
@@ -398,7 +396,7 @@ describe('validateNonInterActiveAuth', () => {
|
||||
});
|
||||
|
||||
it(`prints JSON error when enforced auth mismatches current auth and exits with code ${ExitCodes.FATAL_AUTHENTICATION_ERROR}`, async () => {
|
||||
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
|
||||
mockSettings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
const nonInteractiveConfig = createLocalMockConfig({
|
||||
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
|
||||
getContentGeneratorConfig: vi
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function validateNonInteractiveAuth(
|
||||
try {
|
||||
const effectiveAuthType = configuredAuthType || getAuthTypeFromEnv();
|
||||
|
||||
const enforcedType = settings.merged.security?.auth?.enforcedType;
|
||||
const enforcedType = settings.merged.security.auth.enforcedType;
|
||||
if (enforcedType && effectiveAuthType !== enforcedType) {
|
||||
const message = effectiveAuthType
|
||||
? `The enforced authentication type is '${enforcedType}', but the current type is '${effectiveAuthType}'. Please re-authenticate with the correct type.`
|
||||
|
||||
@@ -121,7 +121,7 @@ export class GeminiAgent {
|
||||
|
||||
async authenticate({ methodId }: acp.AuthenticateRequest): Promise<void> {
|
||||
const method = z.nativeEnum(AuthType).parse(methodId);
|
||||
const selectedAuthType = this.settings.merged.security?.auth?.selectedType;
|
||||
const selectedAuthType = this.settings.merged.security.auth.selectedType;
|
||||
|
||||
// Only clear credentials when switching to a different auth method
|
||||
if (selectedAuthType && selectedAuthType !== method) {
|
||||
@@ -147,7 +147,7 @@ export class GeminiAgent {
|
||||
const config = await this.newSessionConfig(sessionId, cwd, mcpServers);
|
||||
|
||||
let isAuthenticated = false;
|
||||
if (this.settings.merged.security?.auth?.selectedType) {
|
||||
if (this.settings.merged.security.auth.selectedType) {
|
||||
try {
|
||||
await config.refreshAuth(
|
||||
this.settings.merged.security.auth.selectedType,
|
||||
|
||||
@@ -312,7 +312,8 @@ describe('Telemetry SDK', () => {
|
||||
expect(GcpTraceExporter).not.toHaveBeenCalled();
|
||||
|
||||
// 2. Set project ID and emit post_auth event
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
|
||||
vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project');
|
||||
vi.stubEnv('OTLP_GOOGLE_CLOUD_PROJECT', 'test-project');
|
||||
|
||||
const mockCredentials = {
|
||||
client_email: 'test@example.com',
|
||||
|
||||
Reference in New Issue
Block a user