diff --git a/packages/cli/src/ui/colors.ts b/packages/cli/src/ui/colors.ts index c5472efa..19fae1b9 100644 --- a/packages/cli/src/ui/colors.ts +++ b/packages/cli/src/ui/colors.ts @@ -8,6 +8,9 @@ import { themeManager } from './themes/theme-manager.js'; import { ColorsTheme } from './themes/theme.js'; export const Colors: ColorsTheme = { + get type() { + return themeManager.getActiveTheme().colors.type; + }, get Foreground() { return themeManager.getActiveTheme().colors.Foreground; }, diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.tsx index f0626fa9..ba25f2b6 100644 --- a/packages/cli/src/ui/components/SuggestionsDisplay.tsx +++ b/packages/cli/src/ui/components/SuggestionsDisplay.tsx @@ -5,6 +5,7 @@ */ import { Box, Text } from 'ink'; +import { Colors } from '../colors.js'; export interface Suggestion { label: string; value: string; @@ -48,7 +49,7 @@ export function SuggestionsDisplay({ return ( - {scrollOffset > 0 && } + {scrollOffset > 0 && } {visibleSuggestions.map((suggestion, index) => { const originalIndex = startIndex + index; @@ -56,8 +57,8 @@ export function SuggestionsDisplay({ return ( {suggestion.label} diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx index 7e8c5afd..20686040 100644 --- a/packages/cli/src/ui/components/ThemeDialog.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.tsx @@ -32,16 +32,22 @@ export function ThemeDialog({ SettingScope.User, ); - const themeItems = themeManager.getAvailableThemes().map((theme) => ({ - label: theme.active ? `${theme.name} (Active)` : theme.name, - value: theme.name, - })); + // Generate theme items + const themeItems = themeManager.getAvailableThemes().map((theme) => { + const typeString = theme.type.charAt(0).toUpperCase() + theme.type.slice(1); + return { + label: theme.name, + value: theme.name, + themeNameDisplay: theme.name, + themeTypeDisplay: typeString, + }; + }); 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 const initialThemeIndex = themeItems.findIndex( - (item) => - item.value === - (settings.forScope(selectedScope).settings.theme || DEFAULT_THEME.name), + (item) => item.value === (settings.merged.theme || DEFAULT_THEME.name), ); const scopeItems = [ @@ -88,45 +94,49 @@ export function ThemeDialog({ return ( - - {focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '} - {otherScopeModifiedMessage} - - - - {/* Scope Selection */} - - - {focusedSection === 'scope' ? '> ' : ' '}Apply To + {/* Left Column: Selection */} + + + {focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '} + {otherScopeModifiedMessage} + + {/* Scope Selection */} + + + {focusedSection === 'scope' ? '> ' : ' '}Apply To + + + + + + + (Use ↑/↓ arrows and Enter to select, Tab to change focus) + + - - - (Use ↑/↓ arrows and Enter to select, Tab to change focus) - - - - + {/* Right Column: Preview */} + Preview = ({ const hasPending = !toolCalls.every( (t) => t.status === ToolCallStatus.Success, ); - const borderColor = hasPending ? Colors.AccentYellow : Colors.AccentCyan; + const borderColor = hasPending ? Colors.AccentYellow : Colors.AccentPurple; return ( { */ export interface RadioButtonSelectProps { /** An array of items to display as radio options. */ - items: Array>; + items: Array< + RadioSelectItem & { + themeNameDisplay?: string; + themeTypeDisplay?: string; + } + >; /** The initial index selected */ initialIndex?: number; @@ -42,33 +47,6 @@ export interface RadioButtonSelectProps { isFocused?: boolean; } -/** - * Custom indicator component displaying radio button style (◉/○). - */ -function RadioIndicator({ - isSelected = false, -}: InkSelectIndicatorProps): React.JSX.Element { - return ( - - - {isSelected ? '◉' : '○'} - - - ); -} - -/** - * Custom item component for displaying the label with appropriate color. - */ -function RadioItem({ - isSelected = false, - label, -}: InkSelectItemProps): React.JSX.Element { - return ( - {label} - ); -} - /** * A specialized SelectInput component styled to look like radio buttons. * It uses '◉' for selected and '○' for unselected items. @@ -80,7 +58,7 @@ export function RadioButtonSelect({ initialIndex, onSelect, onHighlight, - isFocused, + isFocused, // This prop indicates if the current RadioButtonSelect group is focused }: RadioButtonSelectProps): React.JSX.Element { const handleSelect = (item: RadioSelectItem) => { onSelect(item.value); @@ -90,11 +68,72 @@ export function RadioButtonSelect({ onHighlight(item.value); } }; + + /** + * Custom indicator component displaying radio button style (◉/○). + * Color changes based on whether the item is selected and if its group is focused. + */ + function DynamicRadioIndicator({ + isSelected = false, + }: InkSelectIndicatorProps): React.JSX.Element { + let indicatorColor = Colors.Foreground; // Default for not selected + if (isSelected) { + if (isFocused) { + // Group is focused, selected item is AccentGreen + indicatorColor = Colors.AccentGreen; + } else { + // Group is NOT focused, selected item is Foreground + indicatorColor = Colors.Foreground; + } + } + return ( + + {isSelected ? '●' : '○'} + + ); + } + + /** + * Custom item component for displaying the label. + * Color changes based on whether the item is selected and if its group is focused. + * Now also handles displaying theme type with custom color. + */ + function CustomThemeItemComponent( + props: InkSelectItemProps, + ): React.JSX.Element { + const { isSelected = false, label } = props; + const itemWithThemeProps = props as typeof props & { + themeNameDisplay?: string; + themeTypeDisplay?: string; + }; + + let textColor = Colors.Foreground; + if (isSelected) { + textColor = isFocused ? Colors.AccentGreen : Colors.Foreground; + } + + if ( + itemWithThemeProps.themeNameDisplay && + itemWithThemeProps.themeTypeDisplay + ) { + return ( + + {itemWithThemeProps.themeNameDisplay}{' '} + + {itemWithThemeProps.themeTypeDisplay} + + + ); + } + + return {label}; + } + initialIndex = initialIndex ?? 0; return ( { // Determine the effective theme const effectiveTheme = loadedSettings.merged.theme; diff --git a/packages/cli/src/ui/themes/ansi.ts b/packages/cli/src/ui/themes/ansi.ts index 29ca6469..b5e2015e 100644 --- a/packages/cli/src/ui/themes/ansi.ts +++ b/packages/cli/src/ui/themes/ansi.ts @@ -7,7 +7,8 @@ import { ansiTheme, Theme } from './theme.js'; export const ANSI: Theme = new Theme( - 'ANSI colors only', + 'ANSI', + 'ansi', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/atom-one-dark.ts b/packages/cli/src/ui/themes/atom-one-dark.ts index 5599c01a..d38fbcbd 100644 --- a/packages/cli/src/ui/themes/atom-one-dark.ts +++ b/packages/cli/src/ui/themes/atom-one-dark.ts @@ -7,7 +7,8 @@ import { darkTheme, Theme } from './theme.js'; export const AtomOneDark: Theme = new Theme( - 'Atom One Dark', + 'Atom One', + 'dark', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/dracula.ts b/packages/cli/src/ui/themes/dracula.ts index e8979e70..9597e005 100644 --- a/packages/cli/src/ui/themes/dracula.ts +++ b/packages/cli/src/ui/themes/dracula.ts @@ -8,6 +8,7 @@ import { darkTheme, Theme } from './theme.js'; export const Dracula: Theme = new Theme( 'Dracula', + 'dark', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/github.ts b/packages/cli/src/ui/themes/github.ts index 61d7de65..2a5533bb 100644 --- a/packages/cli/src/ui/themes/github.ts +++ b/packages/cli/src/ui/themes/github.ts @@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js'; export const GitHub: Theme = new Theme( 'GitHub', + 'light', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/googlecode.ts b/packages/cli/src/ui/themes/googlecode.ts index 25dbb8a3..0729d67a 100644 --- a/packages/cli/src/ui/themes/googlecode.ts +++ b/packages/cli/src/ui/themes/googlecode.ts @@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js'; export const GoogleCode: Theme = new Theme( 'Google Code', + 'light', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts index 4a8cc32c..d1f8df9c 100644 --- a/packages/cli/src/ui/themes/theme-manager.ts +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -11,12 +11,12 @@ import { GoogleCode } from './googlecode.js'; import { VS } from './vs.js'; import { VS2015 } from './vs2015.js'; import { XCode } from './xcode.js'; -import { Theme } from './theme.js'; +import { Theme, ThemeType } from './theme.js'; import { ANSI } from './ansi.js'; export interface ThemeDisplay { name: string; - active: boolean; + type: ThemeType; } export const DEFAULT_THEME: Theme = VS2015; @@ -43,9 +43,30 @@ class ThemeManager { * Returns a list of available theme names. */ getAvailableThemes(): ThemeDisplay[] { - return this.availableThemes.map((theme) => ({ + const sortedThemes = [...this.availableThemes].sort((a, b) => { + const typeOrder = (type: ThemeType): number => { + switch (type) { + case 'dark': + return 1; + case 'light': + return 2; + case 'ansi': + return 3; + default: + return 4; + } + }; + + const typeComparison = typeOrder(a.type) - typeOrder(b.type); + if (typeComparison !== 0) { + return typeComparison; + } + return a.name.localeCompare(b.name); + }); + + return sortedThemes.map((theme) => ({ name: theme.name, - active: theme === this.activeTheme, + type: theme.type, })); } diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts index 88868790..582d2e9e 100644 --- a/packages/cli/src/ui/themes/theme.ts +++ b/packages/cli/src/ui/themes/theme.ts @@ -5,7 +5,11 @@ */ import type { CSSProperties } from 'react'; + +export type ThemeType = 'light' | 'dark' | 'ansi'; + export interface ColorsTheme { + type: ThemeType; Background: string; Foreground: string; LightBlue: string; @@ -21,6 +25,7 @@ export interface ColorsTheme { } export const lightTheme: ColorsTheme = { + type: 'light', Background: '#FAFAFA', Foreground: '#3C3C43', LightBlue: '#ADD8E6', @@ -36,6 +41,7 @@ export const lightTheme: ColorsTheme = { }; export const darkTheme: ColorsTheme = { + type: 'dark', Background: '#1E1E2E', Foreground: '#CDD6F4', LightBlue: '#ADD8E6', @@ -51,6 +57,7 @@ export const darkTheme: ColorsTheme = { }; export const ansiTheme: ColorsTheme = { + type: 'ansi', Background: 'black', Foreground: 'white', LightBlue: 'blue', @@ -250,6 +257,7 @@ export class Theme { */ constructor( readonly name: string, + readonly type: ThemeType, rawMappings: Record, readonly colors: ColorsTheme, ) { diff --git a/packages/cli/src/ui/themes/vs.ts b/packages/cli/src/ui/themes/vs.ts index ea0d938d..2faf02a7 100644 --- a/packages/cli/src/ui/themes/vs.ts +++ b/packages/cli/src/ui/themes/vs.ts @@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js'; export const VS: Theme = new Theme( 'VS', + 'light', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/vs2015.ts b/packages/cli/src/ui/themes/vs2015.ts index 93f00ec8..34431abf 100644 --- a/packages/cli/src/ui/themes/vs2015.ts +++ b/packages/cli/src/ui/themes/vs2015.ts @@ -8,6 +8,7 @@ import { darkTheme, Theme } from './theme.js'; export const VS2015: Theme = new Theme( 'VS2015', + 'dark', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/xcode.ts b/packages/cli/src/ui/themes/xcode.ts index 53fd2e5b..26b8cf72 100644 --- a/packages/cli/src/ui/themes/xcode.ts +++ b/packages/cli/src/ui/themes/xcode.ts @@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js'; export const XCode: Theme = new Theme( 'XCode', + 'light', { hljs: { display: 'block',