diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts
index 31673e921a..02c6f3bb04 100644
--- a/packages/cli/src/services/BuiltinCommandLoader.ts
+++ b/packages/cli/src/services/BuiltinCommandLoader.ts
@@ -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
diff --git a/packages/cli/src/ui/commands/colorsCommand.test.ts b/packages/cli/src/ui/commands/colorsCommand.test.ts
new file mode 100644
index 0000000000..63e2dc7389
--- /dev/null
+++ b/packages/cli/src/ui/commands/colorsCommand.test.ts
@@ -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',
+ }),
+ );
+ });
+});
diff --git a/packages/cli/src/ui/commands/colorsCommand.ts b/packages/cli/src/ui/commands/colorsCommand.ts
new file mode 100644
index 0000000000..3ed96a4fbd
--- /dev/null
+++ b/packages/cli/src/ui/commands/colorsCommand.ts
@@ -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);
+ },
+};
diff --git a/packages/cli/src/ui/components/ColorsDisplay.test.tsx b/packages/cli/src/ui/components/ColorsDisplay.test.tsx
new file mode 100644
index 0000000000..38b2ce8f62
--- /dev/null
+++ b/packages/cli/src/ui/components/ColorsDisplay.test.tsx
@@ -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(
+ ,
+ );
+ 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();
+ });
+});
diff --git a/packages/cli/src/ui/components/ColorsDisplay.tsx b/packages/cli/src/ui/components/ColorsDisplay.tsx
new file mode 100644
index 0000000000..49d5d4a76b
--- /dev/null
+++ b/packages/cli/src/ui/components/ColorsDisplay.tsx
@@ -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 = {
+ '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 (
+
+
+
+ /colors - Theme Colors Demo
+
+
+ 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.
+
+
+
+ Active Theme:{' '}
+
+ {activeTheme.name}
+ {' '}
+ ({activeTheme.type})
+
+
+
+
+ {/* Header */}
+
+
+
+ Value
+
+
+
+
+ Name
+
+
+
+
+ Usage
+
+
+
+
+ {/* Standard Section */}
+
+ {standardRows.map((row) => renderStandardRow(row))}
+
+
+ {/* Gradient Section */}
+ {gradientRow && (
+
+ {renderGradientRow(gradientRow)}
+
+ )}
+
+ {/* Background Section */}
+
+
+
+ Background Colors
+
+
+ {backgroundRows.map((row) => renderBackgroundRow(row))}
+
+
+ );
+};
+
+function renderStandardRow({ name, value }: StandardColorRow) {
+ const description = COLOR_DESCRIPTIONS[name] || '';
+ const isHex = value.startsWith('#');
+
+ return (
+
+
+ {value}
+
+
+ {name}
+
+
+ {description}
+
+
+ );
+}
+
+function renderGradientRow({ name, value }: GradientColorRow) {
+ const description = COLOR_DESCRIPTIONS[name] || '';
+
+ return (
+
+
+ {value.map((c, i) => (
+
+ {c}
+ {i < value.length - 1 ? ', ' : ''}
+
+ ))}
+
+
+ {name}
+
+
+ {description}
+
+
+ );
+}
+
+function renderBackgroundRow({ name, value }: BackgroundColorRow) {
+ const description = COLOR_DESCRIPTIONS[name] || '';
+
+ return (
+
+
+
+ {value || 'default'}
+
+
+
+ {name}
+
+
+ {description}
+
+
+ );
+}
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index 458452d795..d02c7acfca 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -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 = ({
tier={itemForDisplay.tier}
/>
)}
+ {itemForDisplay.type === 'colors' && }
{itemForDisplay.type === 'help' && commands && (
)}
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index c3f178ad1b..4595307663 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -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',
diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts
index 55048ef6bc..d0277965c9 100644
--- a/packages/cli/src/ui/types.ts
+++ b/packages/cli/src/ui/types.ts
@@ -360,6 +360,11 @@ export type HistoryItemHooksList = HistoryItemBase & {
}>;
};
+export type HistoryItemColors = HistoryItemBase & {
+ type: 'colors';
+ timestamp: Date;
+};
+
// Using Omit 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;