Feature custom themes logic (#2639)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Ali Al Jufairi
2025-07-20 16:51:18 +09:00
committed by GitHub
parent c0bfa388c5
commit 76b935d598
19 changed files with 1313 additions and 341 deletions

View File

@@ -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: '',
},
[],

View File

@@ -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}>

View File

@@ -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,
);
});

View File

@@ -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