diff --git a/docs/configuration.md b/docs/configuration.md
index d32d41c7d..47fa95723 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -14,6 +14,42 @@ AgentCore projects use JSON configuration files in the `agentcore/` directory.
---
+## Environment Variables
+
+### CLI Theme Configuration
+
+The CLI supports both light and dark mode themes. By default, the CLI attempts to detect your system's color scheme
+preference.
+
+| Variable | Values | Description |
+| ----------------- | ------------------------- | ------------------------------------------ |
+| `AGENTCORE_THEME` | `light`, `dark`, `system` | Set the CLI color theme (default: `system`) |
+
+**Examples:**
+
+```bash
+# Force dark mode
+export AGENTCORE_THEME=dark
+
+# Force light mode
+export AGENTCORE_THEME=light
+
+# Auto-detect from system (default)
+export AGENTCORE_THEME=system
+```
+
+**System Detection:**
+
+When `AGENTCORE_THEME` is set to `system` (or not set), the CLI attempts to detect your terminal's color scheme using:
+
+- `COLORFGBG` environment variable (common in many terminals)
+- `APPLE_INTERFACE_STYLE` on macOS
+- `TERMINAL_LIGHT_MODE` custom indicator
+
+If no indicators are found, the CLI defaults to dark mode (most common for terminal users).
+
+---
+
## agentcore.json
Main project configuration using a **flat resource model**. Agents, memories, and credentials are top-level arrays.
diff --git a/src/cli/tui/App.tsx b/src/cli/tui/App.tsx
index 23511d9d6..73d62f435 100644
--- a/src/cli/tui/App.tsx
+++ b/src/cli/tui/App.tsx
@@ -1,6 +1,6 @@
import { getWorkingDirectory } from '../../lib';
import { createProgram } from '../cli';
-import { LayoutProvider } from './context';
+import { LayoutProvider, ThemeProvider } from './context';
import { MissingProjectMessage, WrongDirectoryMessage, getProjectRootMismatch, projectExists } from './guards';
import { PlaceholderScreen } from './screens/PlaceholderScreen';
import { AddFlow } from './screens/add/AddFlow';
@@ -196,8 +196,10 @@ function AppContent() {
export function App() {
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/src/cli/tui/__tests__/theme.test.ts b/src/cli/tui/__tests__/theme.test.ts
new file mode 100644
index 000000000..7e44e6b17
--- /dev/null
+++ b/src/cli/tui/__tests__/theme.test.ts
@@ -0,0 +1,195 @@
+import {
+ DARK_THEME,
+ LIGHT_THEME,
+ THEME_ENV_VAR,
+ detectSystemTheme,
+ getCurrentThemeColors,
+ getThemeColors,
+ getThemeMode,
+} from '../theme.js';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+describe('theme', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment before each test
+ vi.resetModules();
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ describe('getThemeColors', () => {
+ it('returns dark theme colors for dark mode', () => {
+ const colors = getThemeColors('dark');
+
+ expect(colors).toEqual(DARK_THEME);
+ });
+
+ it('returns light theme colors for light mode', () => {
+ const colors = getThemeColors('light');
+
+ expect(colors).toEqual(LIGHT_THEME);
+ });
+ });
+
+ describe('getThemeMode', () => {
+ it('returns dark when AGENTCORE_THEME is set to dark', () => {
+ process.env[THEME_ENV_VAR] = 'dark';
+
+ expect(getThemeMode()).toBe('dark');
+ });
+
+ it('returns light when AGENTCORE_THEME is set to light', () => {
+ process.env[THEME_ENV_VAR] = 'light';
+
+ expect(getThemeMode()).toBe('light');
+ });
+
+ it('is case insensitive for AGENTCORE_THEME', () => {
+ process.env[THEME_ENV_VAR] = 'LIGHT';
+
+ expect(getThemeMode()).toBe('light');
+
+ process.env[THEME_ENV_VAR] = 'DARK';
+
+ expect(getThemeMode()).toBe('dark');
+ });
+
+ it('falls back to system detection when AGENTCORE_THEME is system', () => {
+ process.env[THEME_ENV_VAR] = 'system';
+ // Clear other env vars that might affect detection
+ delete process.env.COLORFGBG;
+ delete process.env.APPLE_INTERFACE_STYLE;
+ delete process.env.TERMINAL_LIGHT_MODE;
+
+ // Should default to dark when no system indicators
+ expect(getThemeMode()).toBe('dark');
+ });
+
+ it('falls back to system detection when AGENTCORE_THEME is not set', () => {
+ delete process.env[THEME_ENV_VAR];
+ delete process.env.COLORFGBG;
+ delete process.env.APPLE_INTERFACE_STYLE;
+ delete process.env.TERMINAL_LIGHT_MODE;
+
+ // Should default to dark when no system indicators
+ expect(getThemeMode()).toBe('dark');
+ });
+ });
+
+ describe('detectSystemTheme', () => {
+ it('returns dark by default when no indicators present', () => {
+ delete process.env.COLORFGBG;
+ delete process.env.APPLE_INTERFACE_STYLE;
+ delete process.env.TERMINAL_LIGHT_MODE;
+
+ expect(detectSystemTheme()).toBe('dark');
+ });
+
+ it('detects light mode from COLORFGBG with light background', () => {
+ process.env.COLORFGBG = '0;15'; // Black text on white background
+
+ expect(detectSystemTheme()).toBe('light');
+ });
+
+ it('detects dark mode from COLORFGBG with dark background', () => {
+ process.env.COLORFGBG = '15;0'; // White text on black background
+
+ expect(detectSystemTheme()).toBe('dark');
+ });
+
+ it('detects light mode from APPLE_INTERFACE_STYLE', () => {
+ process.env.APPLE_INTERFACE_STYLE = 'Light';
+
+ expect(detectSystemTheme()).toBe('light');
+ });
+
+ it('detects light mode from TERMINAL_LIGHT_MODE=true', () => {
+ process.env.TERMINAL_LIGHT_MODE = 'true';
+
+ expect(detectSystemTheme()).toBe('light');
+ });
+
+ it('detects light mode from TERMINAL_LIGHT_MODE=1', () => {
+ process.env.TERMINAL_LIGHT_MODE = '1';
+
+ expect(detectSystemTheme()).toBe('light');
+ });
+ });
+
+ describe('getCurrentThemeColors', () => {
+ it('returns dark theme colors when mode is dark', () => {
+ process.env[THEME_ENV_VAR] = 'dark';
+
+ const colors = getCurrentThemeColors();
+
+ expect(colors).toEqual(DARK_THEME);
+ });
+
+ it('returns light theme colors when mode is light', () => {
+ process.env[THEME_ENV_VAR] = 'light';
+
+ const colors = getCurrentThemeColors();
+
+ expect(colors).toEqual(LIGHT_THEME);
+ });
+ });
+
+ describe('theme color palettes', () => {
+ it('dark theme has all required color categories', () => {
+ expect(DARK_THEME.status).toBeDefined();
+ expect(DARK_THEME.interactive).toBeDefined();
+ expect(DARK_THEME.text).toBeDefined();
+ });
+
+ it('light theme has all required color categories', () => {
+ expect(LIGHT_THEME.status).toBeDefined();
+ expect(LIGHT_THEME.interactive).toBeDefined();
+ expect(LIGHT_THEME.text).toBeDefined();
+ });
+
+ it('dark theme has all status colors', () => {
+ expect(DARK_THEME.status.success).toBeDefined();
+ expect(DARK_THEME.status.error).toBeDefined();
+ expect(DARK_THEME.status.warning).toBeDefined();
+ expect(DARK_THEME.status.info).toBeDefined();
+ expect(DARK_THEME.status.pending).toBeDefined();
+ });
+
+ it('light theme has all status colors', () => {
+ expect(LIGHT_THEME.status.success).toBeDefined();
+ expect(LIGHT_THEME.status.error).toBeDefined();
+ expect(LIGHT_THEME.status.warning).toBeDefined();
+ expect(LIGHT_THEME.status.info).toBeDefined();
+ expect(LIGHT_THEME.status.pending).toBeDefined();
+ });
+
+ it('dark theme has all interactive colors', () => {
+ expect(DARK_THEME.interactive.selection).toBeDefined();
+ expect(DARK_THEME.interactive.cursor).toBeDefined();
+ expect(DARK_THEME.interactive.highlight).toBeDefined();
+ });
+
+ it('light theme has all interactive colors', () => {
+ expect(LIGHT_THEME.interactive.selection).toBeDefined();
+ expect(LIGHT_THEME.interactive.cursor).toBeDefined();
+ expect(LIGHT_THEME.interactive.highlight).toBeDefined();
+ });
+
+ it('dark theme has all text colors', () => {
+ expect(DARK_THEME.text.primary).toBeDefined();
+ expect(DARK_THEME.text.muted).toBeDefined();
+ expect(DARK_THEME.text.directory).toBeDefined();
+ });
+
+ it('light theme has all text colors', () => {
+ expect(LIGHT_THEME.text.primary).toBeDefined();
+ expect(LIGHT_THEME.text.muted).toBeDefined();
+ expect(LIGHT_THEME.text.directory).toBeDefined();
+ });
+ });
+});
diff --git a/src/cli/tui/context/ThemeContext.tsx b/src/cli/tui/context/ThemeContext.tsx
new file mode 100644
index 000000000..9d1f7ce65
--- /dev/null
+++ b/src/cli/tui/context/ThemeContext.tsx
@@ -0,0 +1,98 @@
+import {
+ type ThemeColors,
+ type ThemeMode,
+ getCurrentThemeColors,
+ getThemeColors,
+ getThemeMode,
+} from '../theme';
+import React, { type ReactNode, createContext, useContext, useMemo, useState } from 'react';
+
+/**
+ * Theme context value interface
+ */
+interface ThemeContextValue {
+ /** Current theme mode ('light' or 'dark') */
+ mode: 'light' | 'dark';
+ /** Current theme colors */
+ colors: ThemeColors;
+ /** Whether the theme was auto-detected from system */
+ isSystemDetected: boolean;
+ /** Set the theme mode manually */
+ setMode: (mode: ThemeMode) => void;
+}
+
+const ThemeContext = createContext({
+ mode: 'dark',
+ colors: getCurrentThemeColors(),
+ isSystemDetected: true,
+ setMode: () => {},
+});
+
+/**
+ * Hook to access the current theme context.
+ * Returns the current theme mode, colors, and a function to change the theme.
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * const { colors, mode } = useTheme();
+ * return Success!;
+ * }
+ * ```
+ */
+// eslint-disable-next-line react-refresh/only-export-components
+export function useTheme(): ThemeContextValue {
+ return useContext(ThemeContext);
+}
+
+interface ThemeProviderProps {
+ /** Initial theme mode (defaults to system detection) */
+ initialMode?: ThemeMode;
+ children: ReactNode;
+}
+
+/**
+ * Theme provider component that manages theme state.
+ * Wraps the application to provide theme context to all child components.
+ *
+ * @example
+ * ```tsx
+ * function App() {
+ * return (
+ *
+ *
+ *
+ * );
+ * }
+ * ```
+ */
+export function ThemeProvider({ initialMode = 'system', children }: ThemeProviderProps) {
+ // Resolve initial mode
+ const resolvedInitialMode = initialMode === 'system' ? getThemeMode() : initialMode;
+ const [mode, setModeState] = useState<'light' | 'dark'>(resolvedInitialMode);
+ const [isSystemDetected, setIsSystemDetected] = useState(initialMode === 'system');
+
+ const setMode = (newMode: ThemeMode) => {
+ if (newMode === 'system') {
+ setModeState(getThemeMode());
+ setIsSystemDetected(true);
+ } else {
+ setModeState(newMode);
+ setIsSystemDetected(false);
+ }
+ };
+
+ const colors = useMemo(() => getThemeColors(mode), [mode]);
+
+ const value = useMemo(
+ () => ({
+ mode,
+ colors,
+ isSystemDetected,
+ setMode,
+ }),
+ [mode, colors, isSystemDetected]
+ );
+
+ return {children};
+}
diff --git a/src/cli/tui/context/__tests__/ThemeContext.test.tsx b/src/cli/tui/context/__tests__/ThemeContext.test.tsx
new file mode 100644
index 000000000..5dd811b4b
--- /dev/null
+++ b/src/cli/tui/context/__tests__/ThemeContext.test.tsx
@@ -0,0 +1,153 @@
+import { DARK_THEME, LIGHT_THEME, THEME_ENV_VAR } from '../../theme.js';
+import { ThemeProvider, useTheme } from '../ThemeContext.js';
+import { render } from 'ink-testing-library';
+import React from 'react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+// Test component that uses the theme hook
+function ThemeConsumer({ onRender }: { onRender: (theme: ReturnType) => void }) {
+ const theme = useTheme();
+ onRender(theme);
+ return null;
+}
+
+describe('ThemeContext', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ vi.resetModules();
+ process.env = { ...originalEnv };
+ // Clear theme-related env vars
+ delete process.env[THEME_ENV_VAR];
+ delete process.env.COLORFGBG;
+ delete process.env.APPLE_INTERFACE_STYLE;
+ delete process.env.TERMINAL_LIGHT_MODE;
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ describe('useTheme', () => {
+ it('provides dark theme colors by default', () => {
+ let capturedTheme: ReturnType | null = null;
+
+ render(
+
+ (capturedTheme = theme)} />
+
+ );
+
+ expect(capturedTheme).not.toBeNull();
+ expect(capturedTheme!.mode).toBe('dark');
+ expect(capturedTheme!.colors).toEqual(DARK_THEME);
+ });
+
+ it('provides light theme colors when initialMode is light', () => {
+ let capturedTheme: ReturnType | null = null;
+
+ render(
+
+ (capturedTheme = theme)} />
+
+ );
+
+ expect(capturedTheme).not.toBeNull();
+ expect(capturedTheme!.mode).toBe('light');
+ expect(capturedTheme!.colors).toEqual(LIGHT_THEME);
+ });
+
+ it('provides dark theme colors when initialMode is dark', () => {
+ let capturedTheme: ReturnType | null = null;
+
+ render(
+
+ (capturedTheme = theme)} />
+
+ );
+
+ expect(capturedTheme).not.toBeNull();
+ expect(capturedTheme!.mode).toBe('dark');
+ expect(capturedTheme!.colors).toEqual(DARK_THEME);
+ });
+
+ it('respects AGENTCORE_THEME environment variable for light mode', () => {
+ process.env[THEME_ENV_VAR] = 'light';
+ let capturedTheme: ReturnType | null = null;
+
+ render(
+
+ (capturedTheme = theme)} />
+
+ );
+
+ expect(capturedTheme).not.toBeNull();
+ expect(capturedTheme!.mode).toBe('light');
+ expect(capturedTheme!.colors).toEqual(LIGHT_THEME);
+ });
+
+ it('sets isSystemDetected to true when using system mode', () => {
+ let capturedTheme: ReturnType | null = null;
+
+ render(
+
+ (capturedTheme = theme)} />
+
+ );
+
+ expect(capturedTheme).not.toBeNull();
+ expect(capturedTheme!.isSystemDetected).toBe(true);
+ });
+
+ it('sets isSystemDetected to false when using explicit mode', () => {
+ let capturedTheme: ReturnType | null = null;
+
+ render(
+
+ (capturedTheme = theme)} />
+
+ );
+
+ expect(capturedTheme).not.toBeNull();
+ expect(capturedTheme!.isSystemDetected).toBe(false);
+ });
+
+ it('provides setMode function', () => {
+ let capturedTheme: ReturnType | null = null;
+
+ render(
+
+ (capturedTheme = theme)} />
+
+ );
+
+ expect(capturedTheme).not.toBeNull();
+ expect(typeof capturedTheme!.setMode).toBe('function');
+ });
+ });
+
+ describe('ThemeProvider', () => {
+ it('renders children', () => {
+ const { lastFrame } = render(
+
+ Test Content
+
+ );
+
+ expect(lastFrame()).toContain('Test Content');
+ });
+
+ it('defaults to system mode when no initialMode provided', () => {
+ let capturedTheme: ReturnType | null = null;
+
+ render(
+
+ (capturedTheme = theme)} />
+
+ );
+
+ expect(capturedTheme).not.toBeNull();
+ expect(capturedTheme!.isSystemDetected).toBe(true);
+ });
+ });
+});
diff --git a/src/cli/tui/context/index.ts b/src/cli/tui/context/index.ts
index 05fe7f29e..cc1823d9a 100644
--- a/src/cli/tui/context/index.ts
+++ b/src/cli/tui/context/index.ts
@@ -1 +1,2 @@
export { LayoutProvider, useLayout, buildLogo } from './LayoutContext';
+export { ThemeProvider, useTheme } from './ThemeContext';
diff --git a/src/cli/tui/theme.ts b/src/cli/tui/theme.ts
index 35a0d7424..b7f13ce36 100644
--- a/src/cli/tui/theme.ts
+++ b/src/cli/tui/theme.ts
@@ -1,42 +1,230 @@
/**
* Centralized color definitions for the TUI.
* All color values should be referenced from here to ensure consistency.
+ * Supports both light and dark mode themes.
*/
/**
+ * Theme mode type - either 'light', 'dark', or 'system' (auto-detect)
+ */
+export type ThemeMode = 'light' | 'dark' | 'system';
+
+/**
+ * Color palette interface for a single theme
+ */
+export interface ThemeColors {
+ status: {
+ success: string;
+ error: string;
+ warning: string;
+ info: string;
+ pending: string;
+ };
+ interactive: {
+ selection: string;
+ cursor: string;
+ highlight: string;
+ };
+ text: {
+ primary: string;
+ muted: string;
+ directory: string;
+ };
+}
+
+/**
+ * Dark mode color palette - optimized for dark terminal backgrounds
+ */
+export const DARK_THEME: ThemeColors = {
+ status: {
+ success: 'green',
+ error: 'red',
+ warning: 'yellow',
+ info: 'blue',
+ pending: 'gray',
+ },
+ interactive: {
+ selection: 'cyan',
+ cursor: 'white',
+ highlight: 'cyan',
+ },
+ text: {
+ primary: 'white',
+ muted: 'gray',
+ directory: 'blue',
+ },
+};
+
+/**
+ * Light mode color palette - optimized for light terminal backgrounds
+ */
+export const LIGHT_THEME: ThemeColors = {
+ status: {
+ success: 'greenBright',
+ error: 'redBright',
+ warning: 'yellowBright',
+ info: 'blueBright',
+ pending: 'blackBright',
+ },
+ interactive: {
+ selection: 'cyanBright',
+ cursor: 'black',
+ highlight: 'cyanBright',
+ },
+ text: {
+ primary: 'black',
+ muted: 'blackBright',
+ directory: 'blueBright',
+ },
+};
+
+/**
+ * Environment variable name for theme configuration
+ */
+export const THEME_ENV_VAR = 'AGENTCORE_THEME';
+
+/**
+ * Detect system color scheme preference.
+ * Checks common environment variables and terminal settings.
+ * Returns 'dark' as default since most terminal users prefer dark mode.
+ */
+export function detectSystemTheme(): 'light' | 'dark' {
+ // Check COLORFGBG environment variable (format: "fg;bg" where higher bg = light)
+ const colorFgBg = process.env.COLORFGBG;
+ if (colorFgBg) {
+ const parts = colorFgBg.split(';');
+ const bg = parseInt(parts[parts.length - 1], 10);
+ // Background colors 0-6 and 8 are typically dark, 7 and 9-15 are light
+ if (!isNaN(bg) && (bg === 7 || (bg >= 9 && bg <= 15))) {
+ return 'light';
+ }
+ }
+
+ // Check macOS appearance (if available via environment)
+ const appleInterfaceStyle = process.env.APPLE_INTERFACE_STYLE;
+ if (appleInterfaceStyle?.toLowerCase() === 'light') {
+ return 'light';
+ }
+
+ // Check for explicit light terminal indicators
+ const termProgram = process.env.TERM_PROGRAM?.toLowerCase();
+ const colorTerm = process.env.COLORTERM?.toLowerCase();
+
+ // Some terminals set specific variables when in light mode
+ if (process.env.TERMINAL_LIGHT_MODE === 'true' || process.env.TERMINAL_LIGHT_MODE === '1') {
+ return 'light';
+ }
+
+ // Default to dark mode (most common for terminal users)
+ return 'dark';
+}
+
+/**
+ * Get the current theme mode from environment or system detection.
+ * Priority: AGENTCORE_THEME env var > system detection
+ */
+export function getThemeMode(): 'light' | 'dark' {
+ const envTheme = process.env[THEME_ENV_VAR]?.toLowerCase();
+
+ if (envTheme === 'light') {
+ return 'light';
+ }
+
+ if (envTheme === 'dark') {
+ return 'dark';
+ }
+
+ if (envTheme === 'system' || !envTheme) {
+ return detectSystemTheme();
+ }
+
+ // Invalid value, default to system detection
+ return detectSystemTheme();
+}
+
+/**
+ * Get the theme colors for the specified mode.
+ */
+export function getThemeColors(mode: 'light' | 'dark'): ThemeColors {
+ return mode === 'light' ? LIGHT_THEME : DARK_THEME;
+}
+
+/**
+ * Get the current theme colors based on environment/system settings.
+ */
+export function getCurrentThemeColors(): ThemeColors {
+ return getThemeColors(getThemeMode());
+}
+
+// Legacy exports for backward compatibility
+// These use the current theme colors dynamically
+
+/**
+ * @deprecated Use useTheme() hook or getCurrentThemeColors() instead
* Semantic status colors for indicating state/progress.
*/
export const STATUS_COLORS = {
- success: 'green',
- error: 'red',
- warning: 'yellow',
- info: 'blue',
- pending: 'gray',
+ get success() {
+ return getCurrentThemeColors().status.success;
+ },
+ get error() {
+ return getCurrentThemeColors().status.error;
+ },
+ get warning() {
+ return getCurrentThemeColors().status.warning;
+ },
+ get info() {
+ return getCurrentThemeColors().status.info;
+ },
+ get pending() {
+ return getCurrentThemeColors().status.pending;
+ },
} as const;
/**
+ * @deprecated Use useTheme() hook or getCurrentThemeColors() instead
* Colors for interactive elements like selections and highlights.
*/
export const INTERACTIVE_COLORS = {
- selection: 'cyan',
- cursor: 'white',
- highlight: 'cyan',
+ get selection() {
+ return getCurrentThemeColors().interactive.selection;
+ },
+ get cursor() {
+ return getCurrentThemeColors().interactive.cursor;
+ },
+ get highlight() {
+ return getCurrentThemeColors().interactive.highlight;
+ },
} as const;
/**
+ * @deprecated Use useTheme() hook or getCurrentThemeColors() instead
* Text colors for general content.
*/
export const TEXT_COLORS = {
- primary: 'white',
- muted: 'gray',
- directory: 'blue',
+ get primary() {
+ return getCurrentThemeColors().text.primary;
+ },
+ get muted() {
+ return getCurrentThemeColors().text.muted;
+ },
+ get directory() {
+ return getCurrentThemeColors().text.directory;
+ },
} as const;
/**
+ * @deprecated Use useTheme() hook or getCurrentThemeColors() instead
* Combined theme object for convenient access.
*/
export const THEME = {
- status: STATUS_COLORS,
- interactive: INTERACTIVE_COLORS,
- text: TEXT_COLORS,
+ get status() {
+ return STATUS_COLORS;
+ },
+ get interactive() {
+ return INTERACTIVE_COLORS;
+ },
+ get text() {
+ return TEXT_COLORS;
+ },
} as const;