summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
authorMiguel Solorio <[email protected]>2025-05-08 16:00:55 -0700
committerGitHub <[email protected]>2025-05-08 16:00:55 -0700
commita685597b70242eb4c6b38d30c5356ad79418176d (patch)
treef62cf6f0322293222c76c7cefba54fcd254ac83c /packages/cli/src
parent6b0ac084b8557d3ad76a33df991b73196d792280 (diff)
UI Polish for theme selector (#294)
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/ui/colors.ts3
-rw-r--r--packages/cli/src/ui/components/SuggestionsDisplay.tsx7
-rw-r--r--packages/cli/src/ui/components/ThemeDialog.tsx86
-rw-r--r--packages/cli/src/ui/components/messages/ToolGroupMessage.tsx2
-rw-r--r--packages/cli/src/ui/components/shared/RadioButtonSelect.tsx101
-rw-r--r--packages/cli/src/ui/hooks/useThemeCommand.ts2
-rw-r--r--packages/cli/src/ui/themes/ansi.ts3
-rw-r--r--packages/cli/src/ui/themes/atom-one-dark.ts3
-rw-r--r--packages/cli/src/ui/themes/dracula.ts1
-rw-r--r--packages/cli/src/ui/themes/github.ts1
-rw-r--r--packages/cli/src/ui/themes/googlecode.ts1
-rw-r--r--packages/cli/src/ui/themes/theme-manager.ts29
-rw-r--r--packages/cli/src/ui/themes/theme.ts8
-rw-r--r--packages/cli/src/ui/themes/vs.ts1
-rw-r--r--packages/cli/src/ui/themes/vs2015.ts1
-rw-r--r--packages/cli/src/ui/themes/xcode.ts1
16 files changed, 170 insertions, 80 deletions
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 (
<Box borderStyle="round" flexDirection="column" paddingX={1} width={width}>
- {scrollOffset > 0 && <Text color="gray">▲</Text>}
+ {scrollOffset > 0 && <Text color={Colors.Foreground}>▲</Text>}
{visibleSuggestions.map((suggestion, index) => {
const originalIndex = startIndex + index;
@@ -56,8 +57,8 @@ export function SuggestionsDisplay({
return (
<Text
key={`${suggestion}-${originalIndex}`}
- color={isActive ? 'black' : 'white'}
- backgroundColor={isActive ? 'blue' : undefined}
+ color={isActive ? Colors.Background : Colors.Foreground}
+ backgroundColor={isActive ? Colors.AccentBlue : undefined}
>
{suggestion.label}
</Text>
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 (
<Box
borderStyle="round"
- borderColor={Colors.AccentCyan}
- flexDirection="column"
+ borderColor={Colors.AccentPurple}
+ flexDirection="row"
padding={1}
- width="50%"
+ width="100%"
>
- <Text bold={focusedSection === 'theme'}>
- {focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
- <Text color={Colors.SubtleComment}>{otherScopeModifiedMessage}</Text>
- </Text>
-
- <RadioButtonSelect
- key={selectInputKey}
- items={themeItems}
- 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
+ {/* Left Column: Selection */}
+ <Box flexDirection="column" width="50%" paddingRight={2}>
+ <Text bold={focusedSection === 'theme'}>
+ {focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
+ <Text color={Colors.SubtleComment}>{otherScopeModifiedMessage}</Text>
</Text>
<RadioButtonSelect
- items={scopeItems}
- initialIndex={0} // Default to User Settings
- onSelect={handleScopeSelect}
- onHighlight={handleScopeHighlight}
- isFocused={focusedSection === 'scope'}
+ key={selectInputKey}
+ items={themeItems}
+ initialIndex={initialThemeIndex}
+ onSelect={handleThemeSelect}
+ onHighlight={onHighlight}
+ isFocused={focusedSection === 'theme'}
/>
- </Box>
- <Box marginTop={1}>
- <Text color={Colors.SubtleComment}>
- (Use ↑/↓ arrows and Enter to select, Tab to change focus)
- </Text>
+ {/* 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, Tab to change focus)
+ </Text>
+ </Box>
</Box>
- <Box marginTop={1} flexDirection="column">
+ {/* Right Column: Preview */}
+ <Box flexDirection="column" width="50%" paddingLeft={3}>
<Text bold>Preview</Text>
<Box
borderStyle="single"
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index 401b8ee0..a9a51232 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -27,7 +27,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
const hasPending = !toolCalls.every(
(t) => t.status === ToolCallStatus.Success,
);
- const borderColor = hasPending ? Colors.AccentYellow : Colors.AccentCyan;
+ const borderColor = hasPending ? Colors.AccentYellow : Colors.AccentPurple;
return (
<Box
diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
index 3db8b678..377be3e3 100644
--- a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
+++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
@@ -27,7 +27,12 @@ export interface RadioSelectItem<T> {
*/
export interface RadioButtonSelectProps<T> {
/** An array of items to display as radio options. */
- items: Array<RadioSelectItem<T>>;
+ items: Array<
+ RadioSelectItem<T> & {
+ themeNameDisplay?: string;
+ themeTypeDisplay?: string;
+ }
+ >;
/** The initial index selected */
initialIndex?: number;
@@ -43,33 +48,6 @@ export interface RadioButtonSelectProps<T> {
}
/**
- * Custom indicator component displaying radio button style (◉/○).
- */
-function RadioIndicator({
- isSelected = false,
-}: InkSelectIndicatorProps): React.JSX.Element {
- return (
- <Box marginRight={1}>
- <Text color={isSelected ? Colors.AccentGreen : Colors.Gray}>
- {isSelected ? '◉' : '○'}
- </Text>
- </Box>
- );
-}
-
-/**
- * Custom item component for displaying the label with appropriate color.
- */
-function RadioItem({
- isSelected = false,
- label,
-}: InkSelectItemProps): React.JSX.Element {
- return (
- <Text color={isSelected ? Colors.AccentGreen : Colors.Gray}>{label}</Text>
- );
-}
-
-/**
* 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<T>({
initialIndex,
onSelect,
onHighlight,
- isFocused,
+ isFocused, // This prop indicates if the current RadioButtonSelect group is focused
}: RadioButtonSelectProps<T>): React.JSX.Element {
const handleSelect = (item: RadioSelectItem<T>) => {
onSelect(item.value);
@@ -90,11 +68,72 @@ export function RadioButtonSelect<T>({
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 (
+ <Box marginRight={1}>
+ <Text color={indicatorColor}>{isSelected ? '●' : '○'}</Text>
+ </Box>
+ );
+ }
+
+ /**
+ * 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 (
+ <Text color={textColor}>
+ {itemWithThemeProps.themeNameDisplay}{' '}
+ <Text color={Colors.SubtleComment}>
+ {itemWithThemeProps.themeTypeDisplay}
+ </Text>
+ </Text>
+ );
+ }
+
+ return <Text color={textColor}>{label}</Text>;
+ }
+
initialIndex = initialIndex ?? 0;
return (
<SelectInput
- indicatorComponent={RadioIndicator}
- itemComponent={RadioItem}
+ indicatorComponent={DynamicRadioIndicator}
+ itemComponent={CustomThemeItemComponent}
items={items}
initialIndex={initialIndex}
onSelect={handleSelect}
diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts
index 0f4ab93b..c32a7c2e 100644
--- a/packages/cli/src/ui/hooks/useThemeCommand.ts
+++ b/packages/cli/src/ui/hooks/useThemeCommand.ts
@@ -19,7 +19,7 @@ interface UseThemeCommandReturn {
}
export const useThemeCommand = (
- loadedSettings: LoadedSettings, // Changed parameter
+ loadedSettings: LoadedSettings,
): UseThemeCommandReturn => {
// 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<string, CSSProperties>,
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',