mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
Save settings to ~/.gemini/settings.json and optionally /your/workspace/.gemini/settings.json (#237)
This commit is contained in:
@@ -20,6 +20,7 @@ import { useStartupWarnings } from './hooks/useAppEffects.js';
|
||||
import { shortenPath, type Config } from '@gemini-code/server';
|
||||
import { Colors } from './colors.js';
|
||||
import { Intro } from './components/Intro.js';
|
||||
import { LoadedSettings } from '../config/settings.js';
|
||||
import { Tips } from './components/Tips.js';
|
||||
import { ConsoleOutput } from './components/ConsolePatcher.js';
|
||||
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
|
||||
@@ -29,10 +30,11 @@ import { isAtCommand } from './utils/commandUtils.js';
|
||||
|
||||
interface AppProps {
|
||||
config: Config;
|
||||
settings: LoadedSettings;
|
||||
cliVersion: string;
|
||||
}
|
||||
|
||||
export const App = ({ config, cliVersion }: AppProps) => {
|
||||
export const App = ({ config, settings, cliVersion }: AppProps) => {
|
||||
const [history, setHistory] = useState<HistoryItem[]>([]);
|
||||
const [startupWarnings, setStartupWarnings] = useState<string[]>([]);
|
||||
const {
|
||||
@@ -40,7 +42,7 @@ export const App = ({ config, cliVersion }: AppProps) => {
|
||||
openThemeDialog,
|
||||
handleThemeSelect,
|
||||
handleThemeHighlight,
|
||||
} = useThemeCommand();
|
||||
} = useThemeCommand(settings);
|
||||
|
||||
const {
|
||||
streamingState,
|
||||
@@ -176,6 +178,7 @@ export const App = ({ config, cliVersion }: AppProps) => {
|
||||
<ThemeDialog
|
||||
onSelect={handleThemeSelect}
|
||||
onHighlight={handleThemeHighlight}
|
||||
settings={settings}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -4,33 +4,87 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { themeManager } from '../themes/theme-manager.js';
|
||||
import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { DiffRenderer } from './messages/DiffRenderer.js';
|
||||
import { colorizeCode } from '../utils/CodeColorizer.js';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
|
||||
interface ThemeDialogProps {
|
||||
/** Callback function when a theme is selected */
|
||||
onSelect: (themeName: string) => void;
|
||||
onSelect: (themeName: string | undefined, scope: SettingScope) => void;
|
||||
|
||||
/** Callback function when a theme is highlighted */
|
||||
onHighlight: (themeName: string) => void;
|
||||
onHighlight: (themeName: string | undefined) => void;
|
||||
/** The settings object */
|
||||
settings: LoadedSettings;
|
||||
}
|
||||
|
||||
export function ThemeDialog({
|
||||
onSelect,
|
||||
onHighlight,
|
||||
settings,
|
||||
}: ThemeDialogProps): React.JSX.Element {
|
||||
const [selectedScope, setSelectedScope] = useState<SettingScope>(
|
||||
SettingScope.User,
|
||||
);
|
||||
|
||||
const themeItems = themeManager.getAvailableThemes().map((theme) => ({
|
||||
label: theme.active ? `${theme.name} (Active)` : theme.name,
|
||||
value: theme.name,
|
||||
}));
|
||||
const initialIndex = themeItems.findIndex(
|
||||
(item) => item.value === themeManager.getActiveTheme().name,
|
||||
const [selectInputKey, setSelectInputKey] = useState(Date.now());
|
||||
|
||||
const initialThemeIndex = themeItems.findIndex(
|
||||
(item) =>
|
||||
item.value ===
|
||||
(settings.forScope(selectedScope).settings.theme || DEFAULT_THEME.name),
|
||||
);
|
||||
|
||||
const scopeItems = [
|
||||
{ label: 'User Settings', value: SettingScope.User },
|
||||
{ label: 'Workspace Settings', value: SettingScope.Workspace },
|
||||
];
|
||||
|
||||
const handleThemeSelect = (themeName: string) => {
|
||||
onSelect(themeName, selectedScope);
|
||||
};
|
||||
|
||||
const handleScopeHighlight = (scope: SettingScope) => {
|
||||
setSelectedScope(scope);
|
||||
setSelectInputKey(Date.now());
|
||||
};
|
||||
|
||||
const handleScopeSelect = (scope: SettingScope) => {
|
||||
handleScopeHighlight(scope);
|
||||
setFocusedSection('theme'); // Reset focus to theme section
|
||||
};
|
||||
|
||||
const [focusedSection, setFocusedSection] = useState<'theme' | 'scope'>(
|
||||
'theme',
|
||||
);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.tab) {
|
||||
setFocusedSection((prev) => (prev === 'theme' ? 'scope' : 'theme'));
|
||||
}
|
||||
});
|
||||
|
||||
let otherScopeModifiedMessage = '';
|
||||
const otherScope =
|
||||
selectedScope === SettingScope.User
|
||||
? SettingScope.Workspace
|
||||
: SettingScope.User;
|
||||
if (settings.forScope(otherScope).settings.theme !== undefined) {
|
||||
otherScopeModifiedMessage =
|
||||
settings.forScope(selectedScope).settings.theme !== undefined
|
||||
? `(Also modified in ${otherScope})`
|
||||
: `(Modified in ${otherScope})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
@@ -39,18 +93,36 @@ export function ThemeDialog({
|
||||
padding={1}
|
||||
width="50%"
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Select Theme</Text>
|
||||
</Box>
|
||||
<Text bold={focusedSection === 'theme'}>
|
||||
{focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
|
||||
<Text color={Colors.SubtleComment}>{otherScopeModifiedMessage}</Text>
|
||||
</Text>
|
||||
|
||||
<RadioButtonSelect
|
||||
key={selectInputKey}
|
||||
items={themeItems}
|
||||
initialIndex={initialIndex}
|
||||
onSelect={onSelect}
|
||||
initialIndex={initialThemeIndex}
|
||||
onSelect={handleThemeSelect} // Use the wrapper handler
|
||||
onHighlight={onHighlight}
|
||||
isFocused={focusedSection === 'theme'}
|
||||
/>
|
||||
{/* Scope Selection */}
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold={focusedSection === 'scope'}>
|
||||
{focusedSection === 'scope' ? '> ' : ' '}Apply To
|
||||
</Text>
|
||||
<RadioButtonSelect
|
||||
items={scopeItems}
|
||||
initialIndex={0} // Default to User Settings
|
||||
onSelect={handleScopeSelect}
|
||||
onHighlight={handleScopeHighlight}
|
||||
isFocused={focusedSection === 'scope'}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.SubtleComment}>
|
||||
(Use ↑/↓ arrows and Enter to select)
|
||||
(Use ↑/↓ arrows and Enter to select, Tab to change focus)
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ export interface RadioButtonSelectProps<T> {
|
||||
|
||||
/** Function called when an item is highlighted. Receives the `value` of the selected item. */
|
||||
onHighlight?: (value: T) => void;
|
||||
|
||||
/** Whether this select input is currently focused and should respond to input. */
|
||||
isFocused?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +80,7 @@ export function RadioButtonSelect<T>({
|
||||
initialIndex,
|
||||
onSelect,
|
||||
onHighlight,
|
||||
isFocused,
|
||||
}: RadioButtonSelectProps<T>): React.JSX.Element {
|
||||
const handleSelect = (item: RadioSelectItem<T>) => {
|
||||
onSelect(item.value);
|
||||
@@ -95,6 +99,7 @@ export function RadioButtonSelect<T>({
|
||||
initialIndex={initialIndex}
|
||||
onSelect={handleSelect}
|
||||
onHighlight={handleHighlight}
|
||||
isFocused={isFocused}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,23 +6,36 @@
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { themeManager } from '../themes/theme-manager.js';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting
|
||||
|
||||
interface UseThemeCommandReturn {
|
||||
isThemeDialogOpen: boolean;
|
||||
openThemeDialog: () => void;
|
||||
handleThemeSelect: (themeName: string) => void;
|
||||
handleThemeHighlight: (themeName: string) => void;
|
||||
handleThemeSelect: (
|
||||
themeName: string | undefined,
|
||||
scope: SettingScope,
|
||||
) => void; // Added scope
|
||||
handleThemeHighlight: (themeName: string | undefined) => void;
|
||||
}
|
||||
|
||||
export const useThemeCommand = (): UseThemeCommandReturn => {
|
||||
const [isThemeDialogOpen, setIsThemeDialogOpen] = useState(false);
|
||||
export const useThemeCommand = (
|
||||
loadedSettings: LoadedSettings, // Changed parameter
|
||||
): UseThemeCommandReturn => {
|
||||
// Determine the effective theme
|
||||
const effectiveTheme = loadedSettings.getMerged().theme;
|
||||
|
||||
// Initial state: Open dialog if no theme is set in either user or workspace settings
|
||||
const [isThemeDialogOpen, setIsThemeDialogOpen] = useState(
|
||||
effectiveTheme === undefined,
|
||||
);
|
||||
// TODO: refactor how theme's are accessed to avoid requiring a forced render.
|
||||
const [, setForceRender] = useState(0);
|
||||
|
||||
const openThemeDialog = useCallback(() => {
|
||||
setIsThemeDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
function applyTheme(themeName: string) {
|
||||
function applyTheme(themeName: string | undefined) {
|
||||
try {
|
||||
themeManager.setActiveTheme(themeName);
|
||||
setForceRender((v) => v + 1); // Trigger potential re-render
|
||||
@@ -31,17 +44,25 @@ export const useThemeCommand = (): UseThemeCommandReturn => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleThemeHighlight = useCallback((themeName: string) => {
|
||||
applyTheme(themeName);
|
||||
}, []);
|
||||
|
||||
const handleThemeSelect = useCallback((themeName: string) => {
|
||||
try {
|
||||
const handleThemeHighlight = useCallback(
|
||||
(themeName: string | undefined) => {
|
||||
applyTheme(themeName);
|
||||
} finally {
|
||||
setIsThemeDialogOpen(false); // Close the dialog
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[applyTheme],
|
||||
); // Added applyTheme to dependencies
|
||||
|
||||
const handleThemeSelect = useCallback(
|
||||
(themeName: string | undefined, scope: SettingScope) => {
|
||||
// Added scope parameter
|
||||
try {
|
||||
loadedSettings.setValue(scope, 'theme', themeName); // Update the merged settings
|
||||
applyTheme(loadedSettings.getMerged().theme); // Apply the current theme
|
||||
} finally {
|
||||
setIsThemeDialogOpen(false); // Close the dialog
|
||||
}
|
||||
},
|
||||
[applyTheme], // Added applyTheme to dependencies
|
||||
);
|
||||
|
||||
return {
|
||||
isThemeDialogOpen,
|
||||
|
||||
@@ -19,8 +19,9 @@ export interface ThemeDisplay {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME: Theme = VS2015;
|
||||
|
||||
class ThemeManager {
|
||||
private static readonly DEFAULT_THEME: Theme = VS2015;
|
||||
private readonly availableThemes: Theme[];
|
||||
private activeTheme: Theme;
|
||||
|
||||
@@ -35,7 +36,7 @@ class ThemeManager {
|
||||
XCode,
|
||||
ANSI,
|
||||
];
|
||||
this.activeTheme = ThemeManager.DEFAULT_THEME;
|
||||
this.activeTheme = DEFAULT_THEME;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,10 +53,8 @@ class ThemeManager {
|
||||
* Sets the active theme.
|
||||
* @param themeName The name of the theme to activate.
|
||||
*/
|
||||
setActiveTheme(themeName: string): void {
|
||||
const foundTheme = this.availableThemes.find(
|
||||
(theme) => theme.name === themeName,
|
||||
);
|
||||
setActiveTheme(themeName: string | undefined): void {
|
||||
const foundTheme = this.findThemeByName(themeName);
|
||||
|
||||
if (foundTheme) {
|
||||
this.activeTheme = foundTheme;
|
||||
@@ -64,6 +63,13 @@ class ThemeManager {
|
||||
}
|
||||
}
|
||||
|
||||
findThemeByName(themeName: string | undefined): Theme | undefined {
|
||||
if (!themeName) {
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
return this.availableThemes.find((theme) => theme.name === themeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active theme object.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user