diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx
index 01acd2d3..c1430bb5 100644
--- a/packages/cli/src/ui/App.test.tsx
+++ b/packages/cli/src/ui/App.test.tsx
@@ -15,6 +15,7 @@ import {
AccessibilitySettings,
} from '@gemini-code/core';
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
+import process from 'node:process';
// Define a more complete mock server config based on actual Config
interface MockServerConfig {
@@ -345,20 +346,52 @@ describe('App UI', () => {
expect(lastFrame()).toContain('Using 2 MCP servers');
});
- it('should display theme dialog if no theme is set in settings', async () => {
- mockSettings = createMockSettings({});
- mockConfig.getDebugMode.mockReturnValue(false);
- mockConfig.getShowMemoryUsage.mockReturnValue(false);
+ describe('when no theme is set', () => {
+ let originalNoColor: string | undefined;
- const { lastFrame, unmount } = render(
- ,
- );
- currentUnmount = unmount;
+ beforeEach(() => {
+ originalNoColor = process.env.NO_COLOR;
+ // Ensure no theme is set for these tests
+ mockSettings = createMockSettings({});
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+ });
- expect(lastFrame()).toContain('Select Theme');
+ afterEach(() => {
+ process.env.NO_COLOR = originalNoColor;
+ });
+
+ it('should display theme dialog if NO_COLOR is not set', async () => {
+ delete process.env.NO_COLOR;
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+
+ expect(lastFrame()).toContain('Select Theme');
+ });
+
+ it('should display a message if NO_COLOR is set', async () => {
+ process.env.NO_COLOR = 'true';
+
+ const { lastFrame, unmount } = render(
+ ,
+ );
+ currentUnmount = unmount;
+
+ expect(lastFrame()).toContain(
+ 'Theme configuration unavailable due to NO_COLOR env variable.',
+ );
+ expect(lastFrame()).not.toContain('Select Theme');
+ });
});
});
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index 84c15c8c..b90f8d70 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -136,7 +136,7 @@ export const App = ({
openThemeDialog,
handleThemeSelect,
handleThemeHighlight,
- } = useThemeCommand(settings, setThemeError);
+ } = useThemeCommand(settings, setThemeError, addItem);
useEffect(() => {
if (config) {
diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts
index 01911b44..c258b0e3 100644
--- a/packages/cli/src/ui/hooks/useThemeCommand.ts
+++ b/packages/cli/src/ui/hooks/useThemeCommand.ts
@@ -7,6 +7,8 @@
import { useState, useCallback, useEffect } from 'react';
import { themeManager } from '../themes/theme-manager.js';
import { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting
+import { type HistoryItem, MessageType } from '../types.js';
+import process from 'node:process';
interface UseThemeCommandReturn {
isThemeDialogOpen: boolean;
@@ -21,34 +23,55 @@ interface UseThemeCommandReturn {
export const useThemeCommand = (
loadedSettings: LoadedSettings,
setThemeError: (error: string | null) => void,
+ addItem: (item: Omit, timestamp: number) => void,
): UseThemeCommandReturn => {
// Determine the effective theme
const effectiveTheme = loadedSettings.merged.theme;
// Initial state: Open dialog if no theme is set in either user or workspace settings
const [isThemeDialogOpen, setIsThemeDialogOpen] = useState(
- effectiveTheme === undefined, // Open dialog if no theme is set initially
+ effectiveTheme === undefined && !process.env.NO_COLOR,
);
// TODO: refactor how theme's are accessed to avoid requiring a forced render.
const [, setForceRender] = useState(0);
// Apply initial theme on component mount
useEffect(() => {
- // Only try to set a theme if one is actually defined.
- // If effectiveTheme was undefined, the dialog is already open due to useState above.
- if (effectiveTheme !== undefined) {
- if (!themeManager.setActiveTheme(effectiveTheme)) {
- setIsThemeDialogOpen(true);
- setThemeError(`Theme "${effectiveTheme}" not found.`);
- } else {
- setThemeError(null); // Clear any previous theme error on success
+ if (effectiveTheme === undefined) {
+ if (process.env.NO_COLOR) {
+ addItem(
+ {
+ type: MessageType.INFO,
+ text: 'Theme configuration unavailable due to NO_COLOR env variable.',
+ },
+ Date.now(),
+ );
}
+ // If no theme is set and NO_COLOR is not set, the dialog is already open.
+ return;
}
- }, [effectiveTheme, setThemeError]); // Re-run if effectiveTheme or setThemeError changes
+
+ if (!themeManager.setActiveTheme(effectiveTheme)) {
+ setIsThemeDialogOpen(true);
+ setThemeError(`Theme "${effectiveTheme}" not found.`);
+ } else {
+ setThemeError(null);
+ }
+ }, [effectiveTheme, setThemeError, addItem]); // Re-run if effectiveTheme or setThemeError changes
const openThemeDialog = useCallback(() => {
+ if (process.env.NO_COLOR) {
+ addItem(
+ {
+ type: MessageType.INFO,
+ text: 'Theme configuration unavailable due to NO_COLOR env variable.',
+ },
+ Date.now(),
+ );
+ return;
+ }
setIsThemeDialogOpen(true);
- }, []);
+ }, [addItem]);
const applyTheme = useCallback(
(themeName: string | undefined) => {
diff --git a/packages/cli/src/ui/themes/no-color.ts b/packages/cli/src/ui/themes/no-color.ts
new file mode 100644
index 00000000..f549f5de
--- /dev/null
+++ b/packages/cli/src/ui/themes/no-color.ts
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Theme, ColorsTheme } from './theme.js';
+
+const noColorColorsTheme: ColorsTheme = {
+ type: 'ansi',
+ Background: '',
+ Foreground: '',
+ LightBlue: '',
+ AccentBlue: '',
+ AccentPurple: '',
+ AccentCyan: '',
+ AccentGreen: '',
+ AccentYellow: '',
+ AccentRed: '',
+ SubtleComment: '',
+ Gray: '',
+};
+
+export const NoColorTheme: Theme = new Theme(
+ 'No Color',
+ 'dark',
+ {
+ hljs: {
+ display: 'block',
+ overflowX: 'auto',
+ padding: '0.5em',
+ },
+ 'hljs-keyword': {},
+ 'hljs-literal': {},
+ 'hljs-symbol': {},
+ 'hljs-name': {},
+ 'hljs-link': {
+ textDecoration: 'underline',
+ },
+ 'hljs-built_in': {},
+ 'hljs-type': {},
+ 'hljs-number': {},
+ 'hljs-class': {},
+ 'hljs-string': {},
+ 'hljs-meta-string': {},
+ 'hljs-regexp': {},
+ 'hljs-template-tag': {},
+ 'hljs-subst': {},
+ 'hljs-function': {},
+ 'hljs-title': {},
+ 'hljs-params': {},
+ 'hljs-formula': {},
+ 'hljs-comment': {
+ fontStyle: 'italic',
+ },
+ 'hljs-quote': {
+ fontStyle: 'italic',
+ },
+ 'hljs-doctag': {},
+ 'hljs-meta': {},
+ 'hljs-meta-keyword': {},
+ 'hljs-tag': {},
+ 'hljs-variable': {},
+ 'hljs-template-variable': {},
+ 'hljs-attr': {},
+ 'hljs-attribute': {},
+ 'hljs-builtin-name': {},
+ 'hljs-section': {},
+ 'hljs-emphasis': {
+ fontStyle: 'italic',
+ },
+ 'hljs-strong': {
+ fontWeight: 'bold',
+ },
+ 'hljs-bullet': {},
+ 'hljs-selector-tag': {},
+ 'hljs-selector-id': {},
+ 'hljs-selector-class': {},
+ 'hljs-selector-attr': {},
+ 'hljs-selector-pseudo': {},
+ 'hljs-addition': {
+ display: 'inline-block',
+ width: '100%',
+ },
+ 'hljs-deletion': {
+ display: 'inline-block',
+ width: '100%',
+ },
+ },
+ noColorColorsTheme,
+);
diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts
index 30f1a62c..10aa53f0 100644
--- a/packages/cli/src/ui/themes/theme-manager.ts
+++ b/packages/cli/src/ui/themes/theme-manager.ts
@@ -17,6 +17,8 @@ import { XCode } from './xcode.js';
import { Theme, ThemeType } from './theme.js';
import { ANSI } from './ansi.js';
import { ANSILight } from './ansi-light.js';
+import { NoColorTheme } from './no-color.js';
+import process from 'node:process';
export interface ThemeDisplay {
name: string;
@@ -110,6 +112,9 @@ class ThemeManager {
* Returns the currently active theme object.
*/
getActiveTheme(): Theme {
+ if (process.env.NO_COLOR) {
+ return NoColorTheme;
+ }
return this.activeTheme;
}
}