Make merged settings non-nullable and fix all lints related to that. (#16647)

This commit is contained in:
Jacob Richman
2026-01-15 09:26:10 -08:00
committed by GitHub
parent 2b6bfe4097
commit f7f38e2b9e
59 changed files with 964 additions and 744 deletions
+2 -2
View File
@@ -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', () => {
+33 -18
View File
@@ -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,
},
},
+27 -27
View File
@@ -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,
]);
+7 -7
View File
@@ -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();
});
+5 -5
View File
@@ -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.',
+3 -3
View File
@@ -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(
+1 -1
View File
@@ -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',
+3 -13
View File
@@ -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');
});
+2 -4
View File
@@ -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];
+2 -2
View File
@@ -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 = {}) => ({
merged: {
hideFooter: false,
showMemoryUsage: false,
...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 = (
+2 -4
View File
@@ -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
];
}
+5 -6
View File
@@ -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();
+1 -1
View File
@@ -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);
+2 -2
View File
@@ -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];
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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)