Skip to content
Merged
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
14 changes: 13 additions & 1 deletion packages/cli/src/test-utils/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
import { AppContext, type AppState } from '../ui/contexts/AppContext.js';
import { createMockSettings } from './settings.js';
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { DefaultLight } from '../ui/themes/default-light.js';
import { pickDefaultThemeName } from '../ui/themes/theme.js';

export const persistentStateMock = new FakePersistentState();

Expand Down Expand Up @@ -150,8 +153,8 @@ const baseMockUiState = {
terminalWidth: 120,
terminalHeight: 40,
currentModel: 'gemini-pro',
terminalBackgroundColor: 'black',
cleanUiDetailsVisible: false,
terminalBackgroundColor: undefined,
activePtyId: undefined,
backgroundShells: new Map(),
backgroundShellHeight: 0,
Expand Down Expand Up @@ -298,6 +301,15 @@ export const renderWithProviders = (
mainAreaWidth,
};

themeManager.setTerminalBackground(baseState.terminalBackgroundColor);
const themeName = pickDefaultThemeName(
baseState.terminalBackgroundColor,
themeManager.getAllThemes(),
DEFAULT_THEME.name,
DefaultLight.name,
);
themeManager.setActiveTheme(themeName);

const finalUIActions = { ...mockUIActions, ...uiActions };

const allToolCalls = (finalUiState.pendingHistoryItems || [])
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export const AppContainer = (props: AppContainerProps) => {
);
coreEvents.off(CoreEvent.AgentsDiscovered, handleAgentsDiscovered);
};
}, []);
}, [settings]);

const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
useConsoleMessages();
Expand Down Expand Up @@ -612,7 +612,7 @@ export const AppContainer = (props: AppContainerProps) => {
);

// Poll for terminal background color changes to auto-switch theme
useTerminalTheme(handleThemeSelect, config);
useTerminalTheme(handleThemeSelect, config, refreshStatic);

const {
authState,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/ui/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const Colors: ColorsTheme = {
return themeManager.getActiveTheme().colors.Foreground;
},
get Background() {
return themeManager.getActiveTheme().colors.Background;
return themeManager.getColors().Background;
},
get LightBlue() {
return themeManager.getActiveTheme().colors.LightBlue;
Expand Down Expand Up @@ -51,7 +51,7 @@ export const Colors: ColorsTheme = {
return themeManager.getActiveTheme().colors.Gray;
},
get DarkGray() {
return themeManager.getActiveTheme().colors.DarkGray;
return themeManager.getColors().DarkGray;
},
get GradientColors() {
return themeManager.getActiveTheme().colors.GradientColors;
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/ui/components/InputPrompt.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1549,7 +1549,6 @@ describe('InputPrompt', () => {
{ color: 'black', name: 'black' },
{ color: '#000000', name: '#000000' },
{ color: '#000', name: '#000' },
{ color: undefined, name: 'default (black)' },
{ color: 'white', name: 'white' },
{ color: '#ffffff', name: '#ffffff' },
{ color: '#fff', name: '#fff' },
Expand Down Expand Up @@ -1619,6 +1618,11 @@ describe('InputPrompt', () => {

const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
{
uiState: {
terminalBackgroundColor: 'black',
} as Partial<UIState>,
},
);

await waitFor(() => {
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/ui/components/InputPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
terminalWidth,
activePtyId,
history,
terminalBackgroundColor,
backgroundShells,
backgroundShellHeight,
shortcutsHelpVisible,
Expand Down Expand Up @@ -1352,7 +1351,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({

const useBackgroundColor = config.getUseBackgroundColor();
const isLowColor = isLowColorDepth();
const terminalBg = terminalBackgroundColor || 'black';
const terminalBg = theme.background.primary || 'black';

// We should fallback to lines if the background color is disabled OR if it is
// enabled but we are in a low color depth terminal where we don't have a safe
Expand Down
47 changes: 19 additions & 28 deletions packages/cli/src/ui/components/ThemeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useCallback, useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js';
import { pickDefaultThemeName } from '../themes/theme.js';
import { pickDefaultThemeName, type Theme } from '../themes/theme.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { DiffRenderer } from './messages/DiffRenderer.js';
import { colorizeCode } from '../utils/CodeColorizer.js';
Expand All @@ -27,7 +27,10 @@ import { useUIState } from '../contexts/UIStateContext.js';

interface ThemeDialogProps {
/** Callback function when a theme is selected */
onSelect: (themeName: string, scope: LoadableSettingScope) => void;
onSelect: (
themeName: string,
scope: LoadableSettingScope,
) => void | Promise<void>;

/** Callback function when the dialog is cancelled */
onCancel: () => void;
Expand All @@ -40,24 +43,21 @@ interface ThemeDialogProps {
terminalWidth: number;
}

import {
getThemeTypeFromBackgroundColor,
resolveColor,
} from '../themes/color-utils.js';
import { resolveColor } from '../themes/color-utils.js';

function generateThemeItem(
name: string,
typeDisplay: string,
themeType: string,
themeBackground: string | undefined,
fullTheme: Theme | undefined,
terminalBackgroundColor: string | undefined,
terminalThemeType: 'light' | 'dark' | undefined,
) {
const isCompatible =
themeType === 'custom' ||
terminalThemeType === undefined ||
themeType === 'ansi' ||
themeType === terminalThemeType;
const isCompatible = fullTheme
? themeManager.isThemeCompatible(fullTheme, terminalBackgroundColor)
: true;

const themeBackground = fullTheme
? resolveColor(fullTheme.colors.Background)
: undefined;

const isBackgroundMatch =
terminalBackgroundColor &&
Expand Down Expand Up @@ -111,26 +111,17 @@ export function ThemeDialog({

const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);

const terminalThemeType = getThemeTypeFromBackgroundColor(
terminalBackgroundColor,
);

// Generate theme items
const themeItems = themeManager
.getAvailableThemes()
.map((theme) => {
const fullTheme = themeManager.getTheme(theme.name);
const themeBackground = fullTheme
? resolveColor(fullTheme.colors.Background)
: undefined;

return generateThemeItem(
theme.name,
capitalize(theme.type),
theme.type,
themeBackground,
fullTheme,
terminalBackgroundColor,
terminalThemeType,
);
})
.sort((a, b) => {
Expand All @@ -149,8 +140,8 @@ export function ThemeDialog({
const safeInitialThemeIndex = initialThemeIndex >= 0 ? initialThemeIndex : 0;

const handleThemeSelect = useCallback(
(themeName: string) => {
onSelect(themeName, selectedScope);
async (themeName: string) => {
await onSelect(themeName, selectedScope);
refreshStatic();
},
[onSelect, selectedScope, refreshStatic],
Expand All @@ -166,8 +157,8 @@ export function ThemeDialog({
}, []);

const handleScopeSelect = useCallback(
(scope: LoadableSettingScope) => {
onSelect(highlightedThemeName, scope);
async (scope: LoadableSettingScope) => {
await onSelect(highlightedThemeName, scope);
refreshStatic();
},
[onSelect, highlightedThemeName, refreshStatic],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,18 @@ exports[`ThemeDialog Snapshots > should render correctly in theme selection mode
│ │
│ > Select Theme Preview │
│ ▲ ┌────────────────────────────────────────────────────────────┐ │
1. ANSI Dark │ │ │
│ 2. ANSI Light Light │ 1 # function │ │
│ 3. Atom One Dark │ 2 def fibonacci(n): │ │
│ 4. Ayu Dark │ 3 a, b = 0, 1 │ │
│ 5. Ayu Light Light │ 4 for _ in range(n): │ │
6. Default Dark │ 5 a, b = b, a + b │ │
│ 7. Default Light Light │ 6 return a │ │
│ 8. Dracula Dark │ │ │
│ 9. GitHub Dark │ 1 - print("Hello, " + name) │ │
│ 10. GitHub Light Light │ 1 + print(f"Hello, {name}!") │ │
│ 11. Google Code Light │ │ │
│ 12. Holiday Dark └────────────────────────────────────────────────────────────┘ │
1. ANSI Dark (Matches terminal) │ │ │
│ 2. Atom One Dark │ 1 # function │ │
│ 3. Ayu Dark │ 2 def fibonacci(n): │ │
│ 4. Default Dark │ 3 a, b = 0, 1 │ │
│ 5. Dracula Dark │ 4 for _ in range(n): │ │
6. GitHub Dark │ 5 a, b = b, a + b │ │
│ 7. Holiday Dark │ 6 return a │ │
│ 8. Shades Of Purple Dark │ │ │
│ 9. ANSI Light Light (Incompatible) │ 1 - print("Hello, " + name) │ │
│ 10. Ayu Light Light (Incompatible) │ 1 + print(f"Hello, {name}!") │ │
│ 11. Default Light Light (Incompatible) │ │ │
│ 12. GitHub Light Light (Incompatible) └────────────────────────────────────────────────────────────┘ │
│ ▼ │
│ │
│ (Use Enter to select, Tab to configure scope, Esc to close) │
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/ui/components/shared/HalfLinePaddedBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type React from 'react';
import { useMemo } from 'react';
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
import { useUIState } from '../../contexts/UIStateContext.js';
import { theme } from '../../semantic-colors.js';
import {
interpolateColor,
resolveColor,
Expand Down Expand Up @@ -52,8 +53,8 @@ const HalfLinePaddedBoxInternal: React.FC<HalfLinePaddedBoxProps> = ({
backgroundOpacity,
children,
}) => {
const { terminalWidth, terminalBackgroundColor } = useUIState();
const terminalBg = terminalBackgroundColor || 'black';
const { terminalWidth } = useUIState();
const terminalBg = theme.background.primary || 'black';

const isLowColor = isLowColorDepth();

Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/ui/contexts/TerminalContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ vi.mock('ink', () => ({
useStdin: () => ({
stdin: mockStdin,
}),
useStdout: () => ({
stdout: {
write: vi.fn(),
},
}),
}));

const TestComponent = ({ onColor }: { onColor: (c: string) => void }) => {
Expand Down
25 changes: 23 additions & 2 deletions packages/cli/src/ui/contexts/TerminalContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { useStdin } from 'ink';
import { useStdin, useStdout } from 'ink';
import type React from 'react';
import {
createContext,
Expand All @@ -20,6 +20,7 @@ export type TerminalEventHandler = (event: string) => void;
interface TerminalContextValue {
subscribe: (handler: TerminalEventHandler) => void;
unsubscribe: (handler: TerminalEventHandler) => void;
queryTerminalBackground: () => Promise<void>;
}

const TerminalContext = createContext<TerminalContextValue | undefined>(
Expand All @@ -38,6 +39,7 @@ export function useTerminalContext() {

export function TerminalProvider({ children }: { children: React.ReactNode }) {
const { stdin } = useStdin();
const { stdout } = useStdout();
const subscribers = useRef<Set<TerminalEventHandler>>(new Set()).current;
const bufferRef = useRef('');

Expand All @@ -55,6 +57,23 @@ export function TerminalProvider({ children }: { children: React.ReactNode }) {
[subscribers],
);

const queryTerminalBackground = useCallback(
async () =>
new Promise<void>((resolve) => {
const handler = () => {
unsubscribe(handler);
resolve();
};
subscribe(handler);
TerminalCapabilityManager.queryBackgroundColor(stdout);
setTimeout(() => {
unsubscribe(handler);
resolve();
}, 100);
}),
[stdout, subscribe, unsubscribe],
);

useEffect(() => {
const handleData = (data: Buffer | string) => {
bufferRef.current +=
Expand Down Expand Up @@ -89,7 +108,9 @@ export function TerminalProvider({ children }: { children: React.ReactNode }) {
}, [stdin, subscribers]);

return (
<TerminalContext.Provider value={{ subscribe, unsubscribe }}>
<TerminalContext.Provider
value={{ subscribe, unsubscribe, queryTerminalBackground }}
>
{children}
</TerminalContext.Provider>
);
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/ui/contexts/UIActionsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import type { SessionInfo } from '../../utils/sessionUtils.js';
import { type NewAgentsChoice } from '../components/NewAgentsNotification.js';

export interface UIActions {
handleThemeSelect: (themeName: string, scope: LoadableSettingScope) => void;
handleThemeSelect: (
themeName: string,
scope: LoadableSettingScope,
) => Promise<void>;
closeThemeDialog: () => void;
handleThemeHighlight: (themeName: string | undefined) => void;
handleAuthSelect: (
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/ui/hooks/useSnowfall.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import type { UIState } from '../contexts/UIStateContext.js';
vi.mock('../themes/theme-manager.js', () => ({
themeManager: {
getActiveTheme: vi.fn(),
setTerminalBackground: vi.fn(),
getAllThemes: vi.fn(() => []),
setActiveTheme: vi.fn(),
},
DEFAULT_THEME: { name: 'Default' },
}));

vi.mock('../themes/holiday.js', () => ({
Expand Down
Loading
Loading