mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 01:51:20 -07:00
feat(ui): add /colors slash command to demo theme colors
This commit is contained in:
@@ -22,6 +22,7 @@ import { agentsCommand } from '../ui/commands/agentsCommand.js';
|
||||
import { authCommand } from '../ui/commands/authCommand.js';
|
||||
import { bugCommand } from '../ui/commands/bugCommand.js';
|
||||
import { chatCommand, debugCommand } from '../ui/commands/chatCommand.js';
|
||||
import { colorsCommand } from '../ui/commands/colorsCommand.js';
|
||||
import { clearCommand } from '../ui/commands/clearCommand.js';
|
||||
import { commandsCommand } from '../ui/commands/commandsCommand.js';
|
||||
import { compressCommand } from '../ui/commands/compressCommand.js';
|
||||
@@ -83,6 +84,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
...(this.config?.isAgentsEnabled() ? [agentsCommand] : []),
|
||||
authCommand,
|
||||
bugCommand,
|
||||
colorsCommand,
|
||||
{
|
||||
...chatCommand,
|
||||
subCommands: isNightlyBuild
|
||||
|
||||
37
packages/cli/src/ui/commands/colorsCommand.test.ts
Normal file
37
packages/cli/src/ui/commands/colorsCommand.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { colorsCommand } from './colorsCommand.js';
|
||||
import { CommandKind, type CommandContext } from './types.js';
|
||||
|
||||
describe('colorsCommand', () => {
|
||||
it('should have the correct metadata', () => {
|
||||
expect(colorsCommand.name).toBe('colors');
|
||||
expect(colorsCommand.description).toBe(
|
||||
'Visualize the current theme colors',
|
||||
);
|
||||
expect(colorsCommand.kind).toBe(CommandKind.BUILT_IN);
|
||||
expect(colorsCommand.autoExecute).toBe(true);
|
||||
});
|
||||
|
||||
it('should add a COLORS message to the UI', () => {
|
||||
const mockAddItem = vi.fn();
|
||||
const mockContext = {
|
||||
ui: {
|
||||
addItem: mockAddItem,
|
||||
},
|
||||
} as unknown as CommandContext;
|
||||
|
||||
void colorsCommand.action?.(mockContext, '');
|
||||
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'colors',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
22
packages/cli/src/ui/commands/colorsCommand.ts
Normal file
22
packages/cli/src/ui/commands/colorsCommand.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { SlashCommand } from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import type { HistoryItemColors } from '../types.js';
|
||||
|
||||
export const colorsCommand: SlashCommand = {
|
||||
name: 'colors',
|
||||
description: 'Visualize the current theme colors',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context) => {
|
||||
context.ui.addItem({
|
||||
type: 'colors',
|
||||
timestamp: new Date(),
|
||||
} as HistoryItemColors);
|
||||
},
|
||||
};
|
||||
93
packages/cli/src/ui/components/ColorsDisplay.test.tsx
Normal file
93
packages/cli/src/ui/components/ColorsDisplay.test.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { ColorsDisplay } from './ColorsDisplay.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { themeManager } from '../themes/theme-manager.js';
|
||||
import type { Theme, ColorsTheme } from '../themes/theme.js';
|
||||
import type { SemanticColors } from '../themes/semantic-tokens.js';
|
||||
|
||||
describe('ColorsDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(themeManager, 'getSemanticColors').mockReturnValue({
|
||||
text: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#cccccc',
|
||||
link: '#0000ff',
|
||||
accent: '#ff00ff',
|
||||
response: '#ffffff',
|
||||
},
|
||||
background: {
|
||||
primary: '#000000',
|
||||
message: '#111111',
|
||||
input: '#222222',
|
||||
diff: {
|
||||
added: '#003300',
|
||||
removed: '#330000',
|
||||
},
|
||||
},
|
||||
border: {
|
||||
default: '#555555',
|
||||
focused: '#0000ff',
|
||||
},
|
||||
ui: {
|
||||
comment: '#666666',
|
||||
symbol: '#cccccc',
|
||||
dark: '#333333',
|
||||
gradient: undefined,
|
||||
},
|
||||
status: {
|
||||
error: '#ff0000',
|
||||
success: '#00ff00',
|
||||
warning: '#ffff00',
|
||||
},
|
||||
});
|
||||
|
||||
vi.spyOn(themeManager, 'getActiveTheme').mockReturnValue({
|
||||
name: 'Test Theme',
|
||||
type: 'dark',
|
||||
colors: {} as unknown as ColorsTheme,
|
||||
semanticColors: {} as unknown as SemanticColors,
|
||||
} as unknown as Theme);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ColorsDisplay />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
// Check for title and description
|
||||
expect(output).toContain('/colors - Theme Colors Demo');
|
||||
expect(output).toContain('visualize how colors are used');
|
||||
|
||||
// Check for Background section
|
||||
expect(output).toContain('Background Colors');
|
||||
|
||||
// Check for active theme name
|
||||
expect(output).toContain('Test Theme');
|
||||
|
||||
// Check for some color names and values
|
||||
expect(output).toContain('text.primary');
|
||||
expect(output).toContain('#ffffff');
|
||||
expect(output).toContain('background.diff.added');
|
||||
expect(output).toContain('#003300');
|
||||
expect(output).toContain('border.default');
|
||||
expect(output).toContain('#555555');
|
||||
|
||||
// Check for some descriptions
|
||||
expect(output).toContain('Primary text color');
|
||||
expect(output).toContain('Standard border color');
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
255
packages/cli/src/ui/components/ColorsDisplay.tsx
Normal file
255
packages/cli/src/ui/components/ColorsDisplay.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { themeManager } from '../themes/theme-manager.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
const COLOR_DESCRIPTIONS: Record<string, string> = {
|
||||
'text.primary': 'Primary text color',
|
||||
'text.secondary': 'Secondary/dimmed text color',
|
||||
'text.link': 'Hyperlink and highlighting color',
|
||||
'text.accent': 'Accent color for emphasis',
|
||||
'text.response': 'Color for model response text',
|
||||
'background.primary': 'Main terminal background color',
|
||||
'background.message': 'Subtle background for message blocks',
|
||||
'background.input': 'Background for the input prompt',
|
||||
'background.diff.added': 'Background for added lines in diffs',
|
||||
'background.diff.removed': 'Background for removed lines in diffs',
|
||||
'border.default': 'Standard border color',
|
||||
'border.focused': 'Border color when an element is focused',
|
||||
'ui.comment': 'Color for code comments and metadata',
|
||||
'ui.symbol': 'Color for technical symbols and UI icons',
|
||||
'ui.dark': 'Deeply dimmed color for subtle UI elements',
|
||||
'ui.gradient': 'Array of colors used for UI gradients',
|
||||
'status.error': 'Color for error messages and critical status',
|
||||
'status.success': 'Color for success messages and positive status',
|
||||
'status.warning': 'Color for warnings and cautionary status',
|
||||
};
|
||||
|
||||
interface StandardColorRow {
|
||||
type: 'standard';
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface GradientColorRow {
|
||||
type: 'gradient';
|
||||
name: string;
|
||||
value: string[];
|
||||
}
|
||||
|
||||
interface BackgroundColorRow {
|
||||
type: 'background';
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const ColorsDisplay: React.FC = () => {
|
||||
const semanticColors = themeManager.getSemanticColors();
|
||||
const activeTheme = themeManager.getActiveTheme();
|
||||
|
||||
const standardRows: StandardColorRow[] = [];
|
||||
const backgroundRows: BackgroundColorRow[] = [];
|
||||
const gradientRow: GradientColorRow | null =
|
||||
semanticColors.ui.gradient && semanticColors.ui.gradient.length > 0
|
||||
? {
|
||||
type: 'gradient',
|
||||
name: 'ui.gradient',
|
||||
value: semanticColors.ui.gradient,
|
||||
}
|
||||
: null;
|
||||
|
||||
// Flatten and categorize the SemanticColors object
|
||||
for (const [category, subColors] of Object.entries(semanticColors)) {
|
||||
if (category === 'ui' && 'gradient' in subColors) {
|
||||
// Handled separately
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(subColors)) {
|
||||
const fullName = `${category}.${name}`;
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
for (const [diffName, diffValue] of Object.entries(value)) {
|
||||
if (typeof diffValue === 'string') {
|
||||
if (category === 'background') {
|
||||
backgroundRows.push({
|
||||
type: 'background',
|
||||
name: `${fullName}.${diffName}`,
|
||||
value: diffValue,
|
||||
});
|
||||
} else {
|
||||
standardRows.push({
|
||||
type: 'standard',
|
||||
name: `${fullName}.${diffName}`,
|
||||
value: diffValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'string') {
|
||||
if (category === 'background') {
|
||||
backgroundRows.push({
|
||||
type: 'background',
|
||||
name: fullName,
|
||||
value,
|
||||
});
|
||||
} else {
|
||||
standardRows.push({
|
||||
type: 'standard',
|
||||
name: fullName,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1} marginY={1}>
|
||||
<Box marginBottom={1} flexDirection="column">
|
||||
<Text bold color={theme.text.accent}>
|
||||
/colors - Theme Colors Demo
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
The purpose of this feature is to visualize how colors are used in the
|
||||
app, test across a variety of Terminals (Mac Terminal, Ghostty,
|
||||
iTerm2, VSCode, etc), and see how the colors change across different
|
||||
themes.
|
||||
</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
Active Theme:{' '}
|
||||
<Text color={theme.text.primary} bold>
|
||||
{activeTheme.name}
|
||||
</Text>{' '}
|
||||
({activeTheme.type})
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Header */}
|
||||
<Box
|
||||
flexDirection="row"
|
||||
marginBottom={1}
|
||||
borderStyle="single"
|
||||
borderColor={theme.border.default}
|
||||
paddingX={1}
|
||||
>
|
||||
<Box width="15%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Value
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width="30%">
|
||||
<Text bold color={theme.text.link}>
|
||||
Name
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text bold color={theme.text.link}>
|
||||
Usage
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Standard Section */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{standardRows.map((row) => renderStandardRow(row))}
|
||||
</Box>
|
||||
|
||||
{/* Gradient Section */}
|
||||
{gradientRow && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{renderGradientRow(gradientRow)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Background Section */}
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
Background Colors
|
||||
</Text>
|
||||
</Box>
|
||||
{backgroundRows.map((row) => renderBackgroundRow(row))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
function renderStandardRow({ name, value }: StandardColorRow) {
|
||||
const description = COLOR_DESCRIPTIONS[name] || '';
|
||||
const isHex = value.startsWith('#');
|
||||
|
||||
return (
|
||||
<Box key={name} flexDirection="row" paddingX={1}>
|
||||
<Box width="15%">
|
||||
<Text color={isHex ? value : theme.text.primary}>{value}</Text>
|
||||
</Box>
|
||||
<Box width="30%">
|
||||
<Text color={theme.text.primary}>{name}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text color={theme.text.secondary}>{description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function renderGradientRow({ name, value }: GradientColorRow) {
|
||||
const description = COLOR_DESCRIPTIONS[name] || '';
|
||||
|
||||
return (
|
||||
<Box key={name} flexDirection="row" paddingX={1}>
|
||||
<Box width="15%" flexDirection="row">
|
||||
{value.map((c, i) => (
|
||||
<Text key={i} color={c}>
|
||||
{c}
|
||||
{i < value.length - 1 ? ', ' : ''}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
<Box width="30%">
|
||||
<Text color={theme.text.primary}>{name}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text color={theme.text.secondary}>{description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBackgroundRow({ name, value }: BackgroundColorRow) {
|
||||
const description = COLOR_DESCRIPTIONS[name] || '';
|
||||
|
||||
return (
|
||||
<Box key={name} flexDirection="row" paddingX={1} marginBottom={1}>
|
||||
<Box
|
||||
width="15%"
|
||||
backgroundColor={value}
|
||||
justifyContent="center"
|
||||
paddingX={1}
|
||||
>
|
||||
<Text color={theme.text.primary} bold>
|
||||
{value || 'default'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width="30%" paddingLeft={1}>
|
||||
<Text color={theme.text.primary}>{name}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text color={theme.text.secondary}>{description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import { HooksList } from './views/HooksList.js';
|
||||
import { ModelMessage } from './messages/ModelMessage.js';
|
||||
import { ThinkingMessage } from './messages/ThinkingMessage.js';
|
||||
import { HintMessage } from './messages/HintMessage.js';
|
||||
import { ColorsDisplay } from './ColorsDisplay.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
@@ -124,6 +125,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
tier={itemForDisplay.tier}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'colors' && <ColorsDisplay />}
|
||||
{itemForDisplay.type === 'help' && commands && (
|
||||
<Help commands={commands} />
|
||||
)}
|
||||
|
||||
@@ -172,6 +172,11 @@ export const useSlashCommandProcessor = (
|
||||
type: 'help',
|
||||
timestamp: message.timestamp,
|
||||
};
|
||||
} else if (message.type === MessageType.COLORS) {
|
||||
historyItemContent = {
|
||||
type: 'colors',
|
||||
timestamp: message.timestamp,
|
||||
};
|
||||
} else if (message.type === MessageType.STATS) {
|
||||
historyItemContent = {
|
||||
type: 'stats',
|
||||
|
||||
@@ -360,6 +360,11 @@ export type HistoryItemHooksList = HistoryItemBase & {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type HistoryItemColors = HistoryItemBase & {
|
||||
type: 'colors';
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
// Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's
|
||||
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
|
||||
// 'tools' in historyItem.
|
||||
@@ -374,6 +379,7 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemWarning
|
||||
| HistoryItemAbout
|
||||
| HistoryItemHelp
|
||||
| HistoryItemColors
|
||||
| HistoryItemToolGroup
|
||||
| HistoryItemStats
|
||||
| HistoryItemModelStats
|
||||
@@ -401,6 +407,7 @@ export enum MessageType {
|
||||
USER = 'user',
|
||||
ABOUT = 'about',
|
||||
HELP = 'help',
|
||||
COLORS = 'colors',
|
||||
STATS = 'stats',
|
||||
MODEL_STATS = 'model_stats',
|
||||
TOOL_STATS = 'tool_stats',
|
||||
@@ -442,6 +449,10 @@ export type Message =
|
||||
timestamp: Date;
|
||||
content?: string; // Optional content, not really used for HELP
|
||||
}
|
||||
| {
|
||||
type: MessageType.COLORS;
|
||||
timestamp: Date;
|
||||
}
|
||||
| {
|
||||
type: MessageType.STATS;
|
||||
timestamp: Date;
|
||||
|
||||
Reference in New Issue
Block a user