mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
Feature custom themes logic (#2639)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
@@ -31,7 +31,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
@@ -41,7 +41,7 @@ describe('AuthDialog', () => {
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
@@ -68,11 +68,17 @@ describe('AuthDialog', () => {
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
@@ -95,11 +101,17 @@ describe('AuthDialog', () => {
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
@@ -122,11 +134,17 @@ describe('AuthDialog', () => {
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
@@ -150,11 +168,17 @@ describe('AuthDialog', () => {
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
@@ -173,11 +197,17 @@ describe('AuthDialog', () => {
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
@@ -198,11 +228,17 @@ describe('AuthDialog', () => {
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
@@ -225,17 +261,19 @@ describe('AuthDialog', () => {
|
||||
const onSelect = vi.fn();
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
@@ -262,11 +300,19 @@ describe('AuthDialog', () => {
|
||||
const onSelect = vi.fn();
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
@@ -296,17 +342,19 @@ describe('AuthDialog', () => {
|
||||
const onSelect = vi.fn();
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: AuthType.USE_GEMINI,
|
||||
customThemes: {},
|
||||
mcpServers: {},
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
settings: { customThemes: {}, mcpServers: {} },
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -36,22 +36,45 @@ export function ThemeDialog({
|
||||
SettingScope.User,
|
||||
);
|
||||
|
||||
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
// Track the currently highlighted theme name
|
||||
const [highlightedThemeName, setHighlightedThemeName] = useState<
|
||||
string | undefined
|
||||
>(settings.merged.theme || DEFAULT_THEME.name);
|
||||
|
||||
// Generate theme items filtered by selected scope
|
||||
const customThemes =
|
||||
selectedScope === SettingScope.User
|
||||
? settings.user.settings.customThemes || {}
|
||||
: settings.merged.customThemes || {};
|
||||
const builtInThemes = themeManager
|
||||
.getAvailableThemes()
|
||||
.filter((theme) => theme.type !== 'custom');
|
||||
const customThemeNames = Object.keys(customThemes);
|
||||
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
// Generate theme items
|
||||
const themeItems = themeManager.getAvailableThemes().map((theme) => ({
|
||||
label: theme.name,
|
||||
value: theme.name,
|
||||
themeNameDisplay: theme.name,
|
||||
themeTypeDisplay: capitalize(theme.type),
|
||||
}));
|
||||
const themeItems = [
|
||||
...builtInThemes.map((theme) => ({
|
||||
label: theme.name,
|
||||
value: theme.name,
|
||||
themeNameDisplay: theme.name,
|
||||
themeTypeDisplay: capitalize(theme.type),
|
||||
})),
|
||||
...customThemeNames.map((name) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
themeNameDisplay: name,
|
||||
themeTypeDisplay: 'Custom',
|
||||
})),
|
||||
];
|
||||
const [selectInputKey, setSelectInputKey] = useState(Date.now());
|
||||
|
||||
// Determine which radio button should be initially selected in the theme list
|
||||
// This should reflect the theme *saved* for the selected scope, or the default
|
||||
// Find the index of the selected theme, but only if it exists in the list
|
||||
const selectedThemeName = settings.merged.theme || DEFAULT_THEME.name;
|
||||
const initialThemeIndex = themeItems.findIndex(
|
||||
(item) => item.value === (settings.merged.theme || DEFAULT_THEME.name),
|
||||
(item) => item.value === selectedThemeName,
|
||||
);
|
||||
// If not found, fallback to the first theme
|
||||
const safeInitialThemeIndex = initialThemeIndex >= 0 ? initialThemeIndex : 0;
|
||||
|
||||
const scopeItems = [
|
||||
{ label: 'User Settings', value: SettingScope.User },
|
||||
@@ -66,6 +89,11 @@ export function ThemeDialog({
|
||||
[onSelect, selectedScope],
|
||||
);
|
||||
|
||||
const handleThemeHighlight = (themeName: string) => {
|
||||
setHighlightedThemeName(themeName);
|
||||
onHighlight(themeName);
|
||||
};
|
||||
|
||||
const handleScopeHighlight = useCallback((scope: SettingScope) => {
|
||||
setSelectedScope(scope);
|
||||
setSelectInputKey(Date.now());
|
||||
@@ -182,7 +210,6 @@ export function ThemeDialog({
|
||||
// The code block is slightly longer than the diff, so give it more space.
|
||||
const codeBlockHeight = Math.ceil(availableHeightForPanes * 0.6);
|
||||
const diffHeight = Math.floor(availableHeightForPanes * 0.4);
|
||||
const themeType = capitalize(themeManager.getActiveTheme().type);
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
@@ -204,9 +231,9 @@ export function ThemeDialog({
|
||||
<RadioButtonSelect
|
||||
key={selectInputKey}
|
||||
items={themeItems}
|
||||
initialIndex={initialThemeIndex}
|
||||
initialIndex={safeInitialThemeIndex}
|
||||
onSelect={handleThemeSelect}
|
||||
onHighlight={onHighlight}
|
||||
onHighlight={handleThemeHighlight}
|
||||
isFocused={currenFocusedSection === 'theme'}
|
||||
maxItemsToShow={8}
|
||||
showScrollArrows={true}
|
||||
@@ -233,40 +260,44 @@ export function ThemeDialog({
|
||||
|
||||
{/* Right Column: Preview */}
|
||||
<Box flexDirection="column" width="55%" paddingLeft={2}>
|
||||
<Text bold>{themeType} Theme Preview</Text>
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={Colors.Gray}
|
||||
paddingTop={includePadding ? 1 : 0}
|
||||
paddingBottom={includePadding ? 1 : 0}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
{colorizeCode(
|
||||
`# python function
|
||||
def fibonacci(n):
|
||||
a, b = 0, 1
|
||||
for _ in range(n):
|
||||
a, b = b, a + b
|
||||
return a`,
|
||||
'python',
|
||||
codeBlockHeight,
|
||||
colorizeCodeWidth,
|
||||
)}
|
||||
<Box marginTop={1} />
|
||||
<DiffRenderer
|
||||
diffContent={`--- a/util.py
|
||||
+++ b/util.py
|
||||
@@ -1,3 +1,3 @@
|
||||
def greet(name):
|
||||
- print("Hello, " + name)
|
||||
+ print(f"Hello, {name}!")
|
||||
`}
|
||||
availableTerminalHeight={diffHeight}
|
||||
terminalWidth={colorizeCodeWidth}
|
||||
/>
|
||||
</Box>
|
||||
<Text bold>Preview</Text>
|
||||
{/* Get the Theme object for the highlighted theme, fallback to default if not found */}
|
||||
{(() => {
|
||||
const previewTheme =
|
||||
themeManager.getTheme(
|
||||
highlightedThemeName || DEFAULT_THEME.name,
|
||||
) || DEFAULT_THEME;
|
||||
return (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={Colors.Gray}
|
||||
paddingTop={includePadding ? 1 : 0}
|
||||
paddingBottom={includePadding ? 1 : 0}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
{colorizeCode(
|
||||
`# function
|
||||
-def fibonacci(n):
|
||||
- a, b = 0, 1
|
||||
- for _ in range(n):
|
||||
- a, b = b, a + b
|
||||
- return a`,
|
||||
'python',
|
||||
codeBlockHeight,
|
||||
colorizeCodeWidth,
|
||||
)}
|
||||
<Box marginTop={1} />
|
||||
<DiffRenderer
|
||||
diffContent={`--- a/old_file.txt\n+++ b/new_file.txt\n@@ -1,6 +1,7 @@\n # function\n-def fibonacci(n):\n- a, b = 0, 1\n- for _ in range(n):\n- a, b = b, a + b\n- return a\n+def fibonacci(n):\n+ a, b = 0, 1\n+ for _ in range(n):\n+ a, b = b, a + b\n+ return a\n+\n+print(fibonacci(10))\n`}
|
||||
availableTerminalHeight={diffHeight}
|
||||
terminalWidth={colorizeCodeWidth}
|
||||
theme={previewTheme}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
|
||||
@@ -44,6 +44,7 @@ index 0000000..e69de29
|
||||
'python',
|
||||
undefined,
|
||||
80,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -71,6 +72,7 @@ index 0000000..e69de29
|
||||
null,
|
||||
undefined,
|
||||
80,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -94,6 +96,7 @@ index 0000000..e69de29
|
||||
null,
|
||||
undefined,
|
||||
80,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ interface DiffRendererProps {
|
||||
tabWidth?: number;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
theme?: import('../../themes/theme.js').Theme;
|
||||
}
|
||||
|
||||
const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization
|
||||
@@ -103,6 +104,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
|
||||
tabWidth = DEFAULT_TAB_WIDTH,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
theme,
|
||||
}) => {
|
||||
if (!diffContent || typeof diffContent !== 'string') {
|
||||
return <Text color={Colors.AccentYellow}>No diff content.</Text>;
|
||||
@@ -146,6 +148,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
|
||||
language,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
theme,
|
||||
);
|
||||
} else {
|
||||
renderedOutput = renderDiffContent(
|
||||
@@ -154,6 +157,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
|
||||
tabWidth,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
theme,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,6 +170,7 @@ const renderDiffContent = (
|
||||
tabWidth = DEFAULT_TAB_WIDTH,
|
||||
availableTerminalHeight: number | undefined,
|
||||
terminalWidth: number,
|
||||
theme?: import('../../themes/theme.js').Theme,
|
||||
) => {
|
||||
// 1. Normalize whitespace (replace tabs with spaces) *before* further processing
|
||||
const normalizedLines = parsedLines.map((line) => ({
|
||||
@@ -246,13 +251,13 @@ const renderDiffContent = (
|
||||
switch (line.type) {
|
||||
case 'add':
|
||||
gutterNumStr = (line.newLine ?? '').toString();
|
||||
color = 'green';
|
||||
color = theme?.colors?.AccentGreen || 'green';
|
||||
prefixSymbol = '+';
|
||||
lastLineNumber = line.newLine ?? null;
|
||||
break;
|
||||
case 'del':
|
||||
gutterNumStr = (line.oldLine ?? '').toString();
|
||||
color = 'red';
|
||||
color = theme?.colors?.AccentRed || 'red';
|
||||
prefixSymbol = '-';
|
||||
// For deletions, update lastLineNumber based on oldLine if it's advancing.
|
||||
// This helps manage gaps correctly if there are multiple consecutive deletions
|
||||
|
||||
Reference in New Issue
Block a user