Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions src/cli/tui/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -196,8 +196,10 @@ function AppContent() {

export function App() {
return (
<LayoutProvider>
<AppContent />
</LayoutProvider>
<ThemeProvider>
<LayoutProvider>
<AppContent />
</LayoutProvider>
</ThemeProvider>
);
}
195 changes: 195 additions & 0 deletions src/cli/tui/__tests__/theme.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
98 changes: 98 additions & 0 deletions src/cli/tui/context/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ThemeContextValue>({
mode: 'dark',
colors: getCurrentThemeColors(),
isSystemDetected: true,
setMode: () => {},

Check failure on line 28 in src/cli/tui/context/ThemeContext.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected empty method '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 <Text color={colors.status.success}>Success!</Text>;
* }
* ```
*/
// 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 (
* <ThemeProvider>
* <MyApp />
* </ThemeProvider>
* );
* }
* ```
*/
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 <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
Loading
Loading