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

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

View File

@@ -313,6 +313,7 @@ describe('App UI', () => {
workspaceSettingsFile,
[],
true,
new Set(),
);
};
@@ -684,7 +685,10 @@ describe('App UI', () => {
it('should display custom contextFileName in footer when set and count is 1', async () => {
mockSettings = createMockSettings({
workspace: { contextFileName: 'AGENTS.md', theme: 'Default' },
workspace: {
context: { fileName: 'AGENTS.md' },
ui: { theme: 'Default' },
},
});
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getAllGeminiMdFilenames.mockReturnValue(['AGENTS.md']);
@@ -706,8 +710,8 @@ describe('App UI', () => {
it('should display a generic message when multiple context files with different names are provided', async () => {
mockSettings = createMockSettings({
workspace: {
contextFileName: ['AGENTS.md', 'CONTEXT.md'],
theme: 'Default',
context: { fileName: ['AGENTS.md', 'CONTEXT.md'] },
ui: { theme: 'Default' },
},
});
mockConfig.getGeminiMdFileCount.mockReturnValue(2);
@@ -732,7 +736,10 @@ describe('App UI', () => {
it('should display custom contextFileName with plural when set and count is > 1', async () => {
mockSettings = createMockSettings({
workspace: { contextFileName: 'MY_NOTES.TXT', theme: 'Default' },
workspace: {
context: { fileName: 'MY_NOTES.TXT' },
ui: { theme: 'Default' },
},
});
mockConfig.getGeminiMdFileCount.mockReturnValue(3);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([
@@ -757,7 +764,10 @@ describe('App UI', () => {
it('should not display context file message if count is 0, even if contextFileName is set', async () => {
mockSettings = createMockSettings({
workspace: { contextFileName: 'ANY_FILE.MD', theme: 'Default' },
workspace: {
context: { fileName: 'ANY_FILE.MD' },
ui: { theme: 'Default' },
},
});
mockConfig.getGeminiMdFileCount.mockReturnValue(0);
mockConfig.getAllGeminiMdFilenames.mockReturnValue([]);
@@ -838,7 +848,7 @@ describe('App UI', () => {
it('should not display Tips component when hideTips is true', async () => {
mockSettings = createMockSettings({
workspace: {
hideTips: true,
ui: { hideTips: true },
},
});
@@ -871,7 +881,7 @@ describe('App UI', () => {
it('should not display Header component when hideBanner is true', async () => {
const { Header } = await import('./components/Header.js');
mockSettings = createMockSettings({
user: { hideBanner: true },
user: { ui: { hideBanner: true } },
});
const { unmount } = renderWithProviders(
@@ -902,7 +912,7 @@ describe('App UI', () => {
it('should not display Footer component when hideFooter is true', async () => {
mockSettings = createMockSettings({
user: { hideFooter: true },
user: { ui: { hideFooter: true } },
});
const { lastFrame, unmount } = renderWithProviders(
@@ -920,9 +930,9 @@ describe('App UI', () => {
it('should show footer if system says show, but workspace and user settings say hide', async () => {
mockSettings = createMockSettings({
system: { hideFooter: false },
user: { hideFooter: true },
workspace: { hideFooter: true },
system: { ui: { hideFooter: false } },
user: { ui: { hideFooter: true } },
workspace: { ui: { hideFooter: true } },
});
const { lastFrame, unmount } = renderWithProviders(
@@ -940,9 +950,9 @@ describe('App UI', () => {
it('should show tips if system says show, but workspace and user settings say hide', async () => {
mockSettings = createMockSettings({
system: { hideTips: false },
user: { hideTips: true },
workspace: { hideTips: true },
system: { ui: { hideTips: false } },
user: { ui: { hideTips: true } },
workspace: { ui: { hideTips: true } },
});
const { unmount } = renderWithProviders(
@@ -1117,9 +1127,13 @@ describe('App UI', () => {
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
mockSettings = createMockSettings({
workspace: {
selectedAuthType: 'USE_GEMINI' as AuthType,
useExternalAuth: false,
theme: 'Default',
security: {
auth: {
selectedType: 'USE_GEMINI' as AuthType,
useExternal: false,
},
},
ui: { theme: 'Default' },
},
});
@@ -1139,9 +1153,13 @@ describe('App UI', () => {
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
mockSettings = createMockSettings({
workspace: {
selectedAuthType: 'USE_GEMINI' as AuthType,
useExternalAuth: true,
theme: 'Default',
security: {
auth: {
selectedType: 'USE_GEMINI' as AuthType,
useExternal: true,
},
},
ui: { theme: 'Default' },
},
});
@@ -1536,8 +1554,8 @@ describe('App UI', () => {
it('should pass debugKeystrokeLogging setting to KeypressProvider', () => {
const mockSettingsWithDebug = createMockSettings({
workspace: {
theme: 'Default',
debugKeystrokeLogging: true,
ui: { theme: 'Default' },
advanced: { debugKeystrokeLogging: true },
},
});
@@ -1553,7 +1571,9 @@ describe('App UI', () => {
const output = lastFrame();
expect(output).toBeDefined();
expect(mockSettingsWithDebug.merged.debugKeystrokeLogging).toBe(true);
expect(mockSettingsWithDebug.merged.advanced?.debugKeystrokeLogging).toBe(
true,
);
});
it('should use default false value when debugKeystrokeLogging is not set', () => {
@@ -1569,7 +1589,9 @@ describe('App UI', () => {
const output = lastFrame();
expect(output).toBeDefined();
expect(mockSettings.merged.debugKeystrokeLogging).toBeUndefined();
expect(
mockSettings.merged.advanced?.debugKeystrokeLogging,
).toBeUndefined();
});
});

View File

@@ -134,7 +134,9 @@ export const AppWrapper = (props: AppProps) => {
<KeypressProvider
kittyProtocolEnabled={kittyProtocolStatus.enabled}
config={props.config}
debugKeystrokeLogging={props.settings.merged.debugKeystrokeLogging}
debugKeystrokeLogging={
props.settings.merged.general?.debugKeystrokeLogging
}
>
<SessionStatsProvider>
<VimModeProvider settings={props.settings}>
@@ -161,7 +163,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const shouldShowIdePrompt =
currentIDE &&
!config.getIdeMode() &&
!settings.merged.hasSeenIdeIntegrationNudge &&
!settings.merged.ide?.hasSeenNudge &&
!idePromptAnswered;
useEffect(() => {
@@ -301,16 +303,21 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
} = useAuthCommand(settings, setAuthError, config);
useEffect(() => {
if (settings.merged.selectedAuthType && !settings.merged.useExternalAuth) {
const error = validateAuthMethod(settings.merged.selectedAuthType);
if (
settings.merged.security?.auth?.selectedType &&
!settings.merged.security?.auth?.useExternal
) {
const error = validateAuthMethod(
settings.merged.security.auth.selectedType,
);
if (error) {
setAuthError(error);
openAuthDialog();
}
}
}, [
settings.merged.selectedAuthType,
settings.merged.useExternalAuth,
settings.merged.security?.auth?.selectedType,
settings.merged.security?.auth?.useExternal,
openAuthDialog,
setAuthError,
]);
@@ -345,14 +352,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
try {
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
process.cwd(),
settings.merged.loadMemoryFromIncludeDirectories
settings.merged.context?.loadMemoryFromIncludeDirectories
? config.getWorkspaceContext().getDirectories()
: [],
config.getDebugMode(),
config.getFileService(),
settings.merged,
config.getExtensionContextFilePaths(),
settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree'
settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree'
config.getFileFilteringOptions(),
);
@@ -510,7 +517,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
}, []);
const getPreferredEditor = useCallback(() => {
const editorType = settings.merged.preferredEditor;
const editorType = settings.merged.general?.preferredEditor;
const isValidEditor = isEditorAvailable(editorType);
if (!isValidEditor) {
openEditorDialog();
@@ -701,7 +708,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const handleGlobalKeypress = useCallback(
(key: Key) => {
// Debug log keystrokes if enabled
if (settings.merged.debugKeystrokeLogging) {
if (settings.merged.general?.debugKeystrokeLogging) {
console.log('[DEBUG] Keystroke:', JSON.stringify(key));
}
@@ -768,7 +775,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
handleSlashCommand,
isAuthenticating,
cancelOngoingRequest,
settings.merged.debugKeystrokeLogging,
settings.merged.general?.debugKeystrokeLogging,
],
);
@@ -884,12 +891,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const branchName = useGitBranchName(config.getTargetDir());
const contextFileNames = useMemo(() => {
const fromSettings = settings.merged.contextFileName;
const fromSettings = settings.merged.context?.fileName;
if (fromSettings) {
return Array.isArray(fromSettings) ? fromSettings : [fromSettings];
}
return getAllGeminiMdFilenames();
}, [settings.merged.contextFileName]);
}, [settings.merged.context?.fileName]);
const initialPrompt = useMemo(() => config.getQuestion(), [config]);
const geminiClient = config.getGeminiClient();
@@ -965,10 +972,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
key={staticKey}
items={[
<Box flexDirection="column" key="header">
{!(settings.merged.hideBanner || config.getScreenReader()) && (
<Header version={version} nightly={nightly} />
)}
{!(settings.merged.hideTips || config.getScreenReader()) && (
{!(
settings.merged.ui?.hideBanner || config.getScreenReader()
) && <Header version={version} nightly={nightly} />}
{!(settings.merged.ui?.hideTips || config.getScreenReader()) && (
<Tips config={config} />
)}
</Box>,
@@ -1300,7 +1307,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
)}
</Box>
)}
{!settings.merged.hideFooter && (
{!settings.merged.ui?.hideFooter && (
<Footer
model={currentModel}
targetDir={config.getTargetDir()}
@@ -1312,7 +1319,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
showErrorDetails={showErrorDetails}
showMemoryUsage={
config.getDebugMode() ||
settings.merged.showMemoryUsage ||
settings.merged.ui?.showMemoryUsage ||
false
}
promptTokenCount={sessionStats.lastPromptTokenCount}

View File

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

View File

@@ -27,7 +27,7 @@ export const aboutCommand: SlashCommand = {
const modelVersion = context.services.config?.getModel() || 'Unknown';
const cliVersion = await getCliVersion();
const selectedAuthType =
context.services.settings.merged.selectedAuthType || '';
context.services.settings.merged.security?.auth?.selectedType || '';
const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || '';
const ideClient =
(context.services.config?.getIdeMode() &&

View File

@@ -104,9 +104,10 @@ export const directoryCommand: SlashCommand = {
config.getDebugMode(),
config.getFileService(),
config.getExtensionContextFilePaths(),
context.services.settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree'
context.services.settings.merged.context?.importFormat ||
'tree', // Use setting or default to 'tree'
config.getFileFilteringOptions(),
context.services.settings.merged.memoryDiscoveryMaxDirs,
context.services.settings.merged.context?.discoveryMaxDirs,
);
config.setUserMemory(memoryContent);
config.setGeminiMdFileCount(fileCount);

View File

@@ -187,7 +187,11 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
Date.now(),
);
if (result.success) {
context.services.settings.setValue(SettingScope.User, 'ideMode', true);
context.services.settings.setValue(
SettingScope.User,
'ide.enabled',
true,
);
// Poll for up to 5 seconds for the extension to activate.
for (let i = 0; i < 10; i++) {
await config.setIdeModeAndSyncConnection(true);
@@ -227,7 +231,11 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
description: 'enable IDE integration',
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => {
context.services.settings.setValue(SettingScope.User, 'ideMode', true);
context.services.settings.setValue(
SettingScope.User,
'ide.enabled',
true,
);
await config.setIdeModeAndSyncConnection(true);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(
@@ -245,7 +253,11 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
description: 'disable IDE integration',
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => {
context.services.settings.setValue(SettingScope.User, 'ideMode', false);
context.services.settings.setValue(
SettingScope.User,
'ide.enabled',
false,
);
await config.setIdeModeAndSyncConnection(false);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(

View File

@@ -92,9 +92,10 @@ export const memoryCommand: SlashCommand = {
config.getDebugMode(),
config.getFileService(),
config.getExtensionContextFilePaths(),
context.services.settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree'
context.services.settings.merged.context?.importFormat ||
'tree', // Use setting or default to 'tree'
config.getFileFilteringOptions(),
context.services.settings.merged.memoryDiscoveryMaxDirs,
context.services.settings.merged.context?.discoveryMaxDirs,
);
config.setUserMemory(memoryContent);
config.setGeminiMdFileCount(fileCount);

View File

@@ -31,7 +31,7 @@ describe('AuthDialog', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
@@ -40,16 +40,21 @@ describe('AuthDialog', () => {
},
{
settings: {
selectedAuthType: AuthType.USE_GEMINI,
security: {
auth: {
selectedType: AuthType.USE_GEMINI,
},
},
},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
@@ -72,8 +77,8 @@ describe('AuthDialog', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
selectedAuthType: undefined,
customThemes: {},
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
@@ -83,15 +88,16 @@ describe('AuthDialog', () => {
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
@@ -110,8 +116,8 @@ describe('AuthDialog', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
selectedAuthType: undefined,
customThemes: {},
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
@@ -121,15 +127,16 @@ describe('AuthDialog', () => {
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
@@ -148,8 +155,8 @@ describe('AuthDialog', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
selectedAuthType: undefined,
customThemes: {},
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
@@ -159,15 +166,16 @@ describe('AuthDialog', () => {
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
@@ -187,8 +195,8 @@ describe('AuthDialog', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
selectedAuthType: undefined,
customThemes: {},
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
@@ -198,15 +206,16 @@ describe('AuthDialog', () => {
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
@@ -221,8 +230,8 @@ describe('AuthDialog', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
selectedAuthType: undefined,
customThemes: {},
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
@@ -232,15 +241,16 @@ describe('AuthDialog', () => {
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
@@ -257,8 +267,8 @@ describe('AuthDialog', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
selectedAuthType: undefined,
customThemes: {},
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
@@ -268,15 +278,16 @@ describe('AuthDialog', () => {
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
@@ -296,7 +307,7 @@ describe('AuthDialog', () => {
const onSelect = vi.fn();
const settings: LoadedSettings = new LoadedSettings(
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
@@ -305,18 +316,19 @@ describe('AuthDialog', () => {
},
{
settings: {
selectedAuthType: undefined,
customThemes: {},
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { lastFrame, stdin, unmount } = renderWithProviders(
@@ -340,7 +352,7 @@ describe('AuthDialog', () => {
const onSelect = vi.fn();
const settings: LoadedSettings = new LoadedSettings(
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
@@ -349,18 +361,19 @@ describe('AuthDialog', () => {
},
{
settings: {
selectedAuthType: undefined,
customThemes: {},
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { lastFrame, stdin, unmount } = renderWithProviders(
@@ -387,7 +400,7 @@ describe('AuthDialog', () => {
const onSelect = vi.fn();
const settings: LoadedSettings = new LoadedSettings(
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
@@ -396,18 +409,19 @@ describe('AuthDialog', () => {
},
{
settings: {
selectedAuthType: AuthType.USE_GEMINI,
customThemes: {},
security: { auth: { selectedType: AuthType.LOGIN_WITH_GOOGLE } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { customThemes: {}, mcpServers: {} },
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
[],
true,
new Set(),
);
const { stdin, unmount } = renderWithProviders(

View File

@@ -83,8 +83,8 @@ export function AuthDialog({
];
const initialAuthIndex = items.findIndex((item) => {
if (settings.merged.selectedAuthType) {
return item.value === settings.merged.selectedAuthType;
if (settings.merged.security?.auth?.selectedType) {
return item.value === settings.merged.security.auth.selectedType;
}
const defaultAuthType = parseDefaultAuthType(
@@ -119,7 +119,7 @@ export function AuthDialog({
if (errorMessage) {
return;
}
if (settings.merged.selectedAuthType === undefined) {
if (settings.merged.security?.auth?.selectedType === undefined) {
// Prevent exiting if no auth method is set
setErrorMessage(
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',

View File

@@ -53,7 +53,7 @@ export function EditorSettingsDialog({
editorSettingsManager.getAvailableEditorDisplays();
const currentPreference =
settings.forScope(selectedScope).settings.preferredEditor;
settings.forScope(selectedScope).settings.general?.preferredEditor;
let editorIndex = currentPreference
? editorItems.findIndex(
(item: EditorDisplay) => item.type === currentPreference,
@@ -87,20 +87,26 @@ export function EditorSettingsDialog({
selectedScope === SettingScope.User
? SettingScope.Workspace
: SettingScope.User;
if (settings.forScope(otherScope).settings.preferredEditor !== undefined) {
if (
settings.forScope(otherScope).settings.general?.preferredEditor !==
undefined
) {
otherScopeModifiedMessage =
settings.forScope(selectedScope).settings.preferredEditor !== undefined
settings.forScope(selectedScope).settings.general?.preferredEditor !==
undefined
? `(Also modified in ${otherScope})`
: `(Modified in ${otherScope})`;
}
let mergedEditorName = 'None';
if (
settings.merged.preferredEditor &&
isEditorAvailable(settings.merged.preferredEditor)
settings.merged.general?.preferredEditor &&
isEditorAvailable(settings.merged.general?.preferredEditor)
) {
mergedEditorName =
EDITOR_DISPLAY_NAMES[settings.merged.preferredEditor as EditorType];
EDITOR_DISPLAY_NAMES[
settings.merged.general?.preferredEditor as EditorType
];
}
return (

View File

@@ -40,7 +40,7 @@ const createMockSettings = (
) =>
new LoadedSettings(
{
settings: { customThemes: {}, mcpServers: {}, ...systemSettings },
settings: { ui: { customThemes: {} }, mcpServers: {}, ...systemSettings },
path: '/system/settings.json',
},
{
@@ -49,18 +49,23 @@ const createMockSettings = (
},
{
settings: {
customThemes: {},
ui: { customThemes: {} },
mcpServers: {},
...userSettings,
},
path: '/user/settings.json',
},
{
settings: { customThemes: {}, mcpServers: {}, ...workspaceSettings },
settings: {
ui: { customThemes: {} },
mcpServers: {},
...workspaceSettings,
},
path: '/workspace/settings.json',
},
[],
true,
new Set(),
);
vi.mock('../contexts/SettingsContext.js', async () => {
@@ -156,7 +161,11 @@ describe('SettingsDialog', () => {
) =>
new LoadedSettings(
{
settings: { customThemes: {}, mcpServers: {}, ...systemSettings },
settings: {
ui: { customThemes: {} },
mcpServers: {},
...systemSettings,
},
path: '/system/settings.json',
},
{
@@ -165,18 +174,23 @@ describe('SettingsDialog', () => {
},
{
settings: {
customThemes: {},
ui: { customThemes: {} },
mcpServers: {},
...userSettings,
},
path: '/user/settings.json',
},
{
settings: { customThemes: {}, mcpServers: {}, ...workspaceSettings },
settings: {
ui: { customThemes: {} },
mcpServers: {},
...workspaceSettings,
},
path: '/workspace/settings.json',
},
[],
true,
new Set(),
);
describe('Initial Rendering', () => {
@@ -392,11 +406,11 @@ describe('SettingsDialog', () => {
// Wait for initial render
await waitFor(() => {
expect(lastFrame()).toContain('Hide Window Title');
expect(lastFrame()).toContain('Vim Mode');
});
// The UI should show the settings section is active and scope section is inactive
expect(lastFrame()).toContain('● Hide Window Title'); // Settings section active
expect(lastFrame()).toContain('● Vim Mode'); // Settings section active
expect(lastFrame()).toContain(' Apply To'); // Scope section inactive
// This test validates the initial state - scope selection behavior
@@ -814,11 +828,11 @@ describe('SettingsDialog', () => {
// Wait for initial render
await waitFor(() => {
expect(lastFrame()).toContain('Hide Window Title');
expect(lastFrame()).toContain('Vim Mode');
});
// Verify initial state: settings section active, scope section inactive
expect(lastFrame()).toContain('● Hide Window Title'); // Settings section active
expect(lastFrame()).toContain('● Vim Mode'); // Settings section active
expect(lastFrame()).toContain(' Apply To'); // Scope section inactive
// This test validates the rendered UI structure for tab navigation
@@ -876,12 +890,12 @@ describe('SettingsDialog', () => {
// Wait for initial render
await waitFor(() => {
expect(lastFrame()).toContain('Hide Window Title');
expect(lastFrame()).toContain('Vim Mode');
});
// Verify the complete UI is rendered with all necessary sections
expect(lastFrame()).toContain('Settings'); // Title
expect(lastFrame()).toContain('● Hide Window Title'); // Active setting
expect(lastFrame()).toContain('● Vim Mode'); // Active setting
expect(lastFrame()).toContain('Apply To'); // Scope section
expect(lastFrame()).toContain('1. User Settings'); // Scope options
expect(lastFrame()).toContain(

View File

@@ -153,7 +153,7 @@ export function SettingsDialog({
);
// Special handling for vim mode to sync with VimModeContext
if (key === 'vimMode' && newValue !== vimEnabled) {
if (key === 'general.vimMode' && newValue !== vimEnabled) {
// Call toggleVimEnabled to sync the VimModeContext local state
toggleVimEnabled().catch((error) => {
console.error('Failed to toggle vim mode:', error);

View File

@@ -46,13 +46,13 @@ export function ThemeDialog({
// Track the currently highlighted theme name
const [highlightedThemeName, setHighlightedThemeName] = useState<
string | undefined
>(settings.merged.theme || DEFAULT_THEME.name);
>(settings.merged.ui?.theme || DEFAULT_THEME.name);
// Generate theme items filtered by selected scope
const customThemes =
selectedScope === SettingScope.User
? settings.user.settings.customThemes || {}
: settings.merged.customThemes || {};
? settings.user.settings.ui?.customThemes || {}
: settings.merged.ui?.customThemes || {};
const builtInThemes = themeManager
.getAvailableThemes()
.filter((theme) => theme.type !== 'custom');
@@ -76,7 +76,7 @@ export function ThemeDialog({
const [selectInputKey, setSelectInputKey] = useState(Date.now());
// Find the index of the selected theme, but only if it exists in the list
const selectedThemeName = settings.merged.theme || DEFAULT_THEME.name;
const selectedThemeName = settings.merged.ui?.theme || DEFAULT_THEME.name;
const initialThemeIndex = themeItems.findIndex(
(item) => item.value === selectedThemeName,
);
@@ -128,7 +128,7 @@ export function ThemeDialog({
// Generate scope message for theme setting
const otherScopeModifiedMessage = getScopeMessageForSetting(
'theme',
'ui.theme',
selectedScope,
settings,
);

View File

@@ -32,7 +32,7 @@ export const VimModeProvider = ({
children: React.ReactNode;
settings: LoadedSettings;
}) => {
const initialVimEnabled = settings.merged.vimMode ?? false;
const initialVimEnabled = settings.merged.general?.vimMode ?? false;
const [vimEnabled, setVimEnabled] = useState(initialVimEnabled);
const [vimMode, setVimMode] = useState<VimMode>(
initialVimEnabled ? 'NORMAL' : 'INSERT',
@@ -40,13 +40,13 @@ export const VimModeProvider = ({
useEffect(() => {
// Initialize vimEnabled from settings on mount
const enabled = settings.merged.vimMode ?? false;
const enabled = settings.merged.general?.vimMode ?? false;
setVimEnabled(enabled);
// When vim mode is enabled, always start in NORMAL mode
if (enabled) {
setVimMode('NORMAL');
}
}, [settings.merged.vimMode]);
}, [settings.merged.general?.vimMode]);
const toggleVimEnabled = useCallback(async () => {
const newValue = !vimEnabled;
@@ -55,7 +55,7 @@ export const VimModeProvider = ({
if (newValue) {
setVimMode('NORMAL');
}
await settings.setValue(SettingScope.User, 'vimMode', newValue);
await settings.setValue(SettingScope.User, 'general.vimMode', newValue);
return newValue;
}, [vimEnabled, settings]);

View File

@@ -19,7 +19,7 @@ export const useAuthCommand = (
config: Config,
) => {
const [isAuthDialogOpen, setIsAuthDialogOpen] = useState(
settings.merged.selectedAuthType === undefined,
settings.merged.security?.auth?.selectedType === undefined,
);
const openAuthDialog = useCallback(() => {
@@ -30,7 +30,7 @@ export const useAuthCommand = (
useEffect(() => {
const authFlow = async () => {
const authType = settings.merged.selectedAuthType;
const authType = settings.merged.security?.auth?.selectedType;
if (isAuthDialogOpen || !authType) {
return;
}
@@ -55,7 +55,7 @@ export const useAuthCommand = (
if (authType) {
await clearCachedCredentialFile();
settings.setValue(scope, 'selectedAuthType', authType);
settings.setValue(scope, 'security.auth.selectedType', authType);
if (
authType === AuthType.LOGIN_WITH_GOOGLE &&
config.isBrowserLaunchSuppressed()

View File

@@ -22,7 +22,10 @@ export const useFolderTrust = (
const [isFolderTrustDialogOpen, setIsFolderTrustDialogOpen] = useState(false);
const [isRestarting, setIsRestarting] = useState(false);
const { folderTrust, folderTrustFeature } = settings.merged;
const folderTrust = settings.merged.security?.folderTrust?.enabled;
const folderTrustFeature =
settings.merged.security?.folderTrust?.featureEnabled;
useEffect(() => {
const trusted = isWorkspaceTrusted({
folderTrust,

View File

@@ -32,7 +32,7 @@ export function createShowMemoryAction(
const currentMemory = config.getUserMemory();
const fileCount = config.getGeminiMdFileCount();
const contextFileName = settings.merged.contextFileName;
const contextFileName = settings.merged.context?.fileName;
const contextFileNames = Array.isArray(contextFileName)
? contextFileName
: [contextFileName];

View File

@@ -29,14 +29,14 @@ export const useThemeCommand = (
// Check for invalid theme configuration on startup
useEffect(() => {
const effectiveTheme = loadedSettings.merged.theme;
const effectiveTheme = loadedSettings.merged.ui?.theme;
if (effectiveTheme && !themeManager.findThemeByName(effectiveTheme)) {
setIsThemeDialogOpen(true);
setThemeError(`Theme "${effectiveTheme}" not found.`);
} else {
setThemeError(null);
}
}, [loadedSettings.merged.theme, setThemeError]);
}, [loadedSettings.merged.ui?.theme, setThemeError]);
const openThemeDialog = useCallback(() => {
if (process.env['NO_COLOR']) {
@@ -77,8 +77,8 @@ export const useThemeCommand = (
try {
// Merge user and workspace custom themes (workspace takes precedence)
const mergedCustomThemes = {
...(loadedSettings.user.settings.customThemes || {}),
...(loadedSettings.workspace.settings.customThemes || {}),
...(loadedSettings.user.settings.ui?.customThemes || {}),
...(loadedSettings.workspace.settings.ui?.customThemes || {}),
};
// Only allow selecting themes available in the merged custom themes or built-in themes
const isBuiltIn = themeManager.findThemeByName(themeName);
@@ -88,11 +88,11 @@ export const useThemeCommand = (
setIsThemeDialogOpen(true);
return;
}
loadedSettings.setValue(scope, 'theme', themeName); // Update the merged settings
if (loadedSettings.merged.customThemes) {
themeManager.loadCustomThemes(loadedSettings.merged.customThemes);
loadedSettings.setValue(scope, 'ui.theme', themeName); // Update the merged settings
if (loadedSettings.merged.ui?.customThemes) {
themeManager.loadCustomThemes(loadedSettings.merged.ui?.customThemes);
}
applyTheme(loadedSettings.merged.theme); // Apply the current theme
applyTheme(loadedSettings.merged.ui?.theme); // Apply the current theme
setThemeError(null);
} finally {
setIsThemeDialogOpen(false); // Close the dialog

View File

@@ -20,7 +20,7 @@ export function useWorkspaceMigration(settings: LoadedSettings) {
);
useEffect(() => {
if (!settings.merged.extensionManagement) {
if (!settings.merged.experimental?.extensionManagement) {
return;
}
const cwd = process.cwd();
@@ -33,7 +33,10 @@ export function useWorkspaceMigration(settings: LoadedSettings) {
setShowWorkspaceMigrationDialog(true);
console.log(settings.merged.extensions);
}
}, [settings.merged.extensions, settings.merged.extensionManagement]);
}, [
settings.merged.extensions,
settings.merged.experimental?.extensionManagement,
]);
const onWorkspaceMigrationDialogOpen = () => {
const userSettings = settings.forScope(SettingScope.User);

View File

@@ -134,7 +134,7 @@ export function colorizeCode(
): React.ReactNode {
const codeToHighlight = code.replace(/\n$/, '');
const activeTheme = theme || themeManager.getActiveTheme();
const showLineNumbers = settings?.merged.showLineNumbers ?? true;
const showLineNumbers = settings?.merged.ui?.showLineNumbers ?? true;
try {
// Render the HAST tree using the adapted theme

View File

@@ -25,6 +25,7 @@ describe('<MarkdownDisplay />', () => {
{ path: '', settings: {} },
[],
true,
new Set(),
);
beforeEach(() => {
@@ -224,10 +225,11 @@ Another paragraph.
const settings = new LoadedSettings(
{ path: '', settings: {} },
{ path: '', settings: {} },
{ path: '', settings: { showLineNumbers: false } },
{ path: '', settings: { ui: { showLineNumbers: false } } },
{ path: '', settings: {} },
[],
true,
new Set(),
);
const { lastFrame } = render(