mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(extensions): add support for custom themes in extensions (#17327)
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
# Themes Example
|
||||||
|
|
||||||
|
This is an example of a Gemini CLI extension that adds a custom theme.
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
1. Link this extension:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gemini extensions link packages/cli/src/commands/extensions/examples/themes-example
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set the theme in your settings file (`~/.gemini/config.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ui:
|
||||||
|
theme: 'shades-of-green-theme (themes-example)'
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can set it through the UI by running `gemini` and then
|
||||||
|
typing `/theme` and pressing Enter.
|
||||||
|
|
||||||
|
3. **Observe the Changes:**
|
||||||
|
|
||||||
|
After setting the theme, you should see the changes reflected in the Gemini
|
||||||
|
CLI's UI. The background will be a dark green, the primary text a lighter
|
||||||
|
green, and various other UI elements will display different shades of green,
|
||||||
|
as defined in this extension's `gemini-extension.json` file.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "themes-example",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"name": "shades-of-green-theme",
|
||||||
|
"type": "custom",
|
||||||
|
"background": {
|
||||||
|
"primary": "#1a362a"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"primary": "#a6e3a1",
|
||||||
|
"secondary": "#6e8e7a",
|
||||||
|
"link": "#89e689"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"success": "#76c076",
|
||||||
|
"warning": "#d9e689",
|
||||||
|
"error": "#b34e4e"
|
||||||
|
},
|
||||||
|
"border": {
|
||||||
|
"default": "#4a6c5a"
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"comment": "#6e8e7a"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import {
|
||||||
|
beforeAll,
|
||||||
|
afterAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
afterEach,
|
||||||
|
} from 'vitest';
|
||||||
|
import { createExtension } from '../test-utils/createExtension.js';
|
||||||
|
import { ExtensionManager } from './extension-manager.js';
|
||||||
|
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
|
||||||
|
import { GEMINI_DIR, type Config } from '@google/gemini-cli-core';
|
||||||
|
import { createTestMergedSettings, SettingScope } from './settings.js';
|
||||||
|
|
||||||
|
describe('ExtensionManager theme loading', () => {
|
||||||
|
let extensionManager: ExtensionManager;
|
||||||
|
let userExtensionsDir: string;
|
||||||
|
let tempHomeDir: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
tempHomeDir = await fs.promises.mkdtemp(
|
||||||
|
path.join(fs.realpathSync('/tmp'), 'gemini-cli-test-'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (tempHomeDir) {
|
||||||
|
await fs.promises.rm(tempHomeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env['GEMINI_CLI_HOME'] = tempHomeDir;
|
||||||
|
userExtensionsDir = path.join(tempHomeDir, GEMINI_DIR, 'extensions');
|
||||||
|
// Ensure userExtensionsDir is clean for each test
|
||||||
|
fs.rmSync(userExtensionsDir, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
||||||
|
|
||||||
|
extensionManager = new ExtensionManager({
|
||||||
|
settings: createTestMergedSettings({
|
||||||
|
experimental: { extensionConfig: true },
|
||||||
|
security: { blockGitExtensions: false },
|
||||||
|
admin: { extensions: { enabled: true }, mcp: { enabled: true } },
|
||||||
|
tools: { enableHooks: true },
|
||||||
|
}),
|
||||||
|
requestConsent: async () => true,
|
||||||
|
requestSetting: async () => '',
|
||||||
|
workspaceDir: tempHomeDir,
|
||||||
|
enabledExtensionOverrides: [],
|
||||||
|
});
|
||||||
|
vi.clearAllMocks();
|
||||||
|
themeManager.clearExtensionThemes();
|
||||||
|
themeManager.loadCustomThemes({});
|
||||||
|
themeManager.setActiveTheme(DEFAULT_THEME.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env['GEMINI_CLI_HOME'];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register themes from an extension when started', async () => {
|
||||||
|
const registerSpy = vi.spyOn(themeManager, 'registerExtensionThemes');
|
||||||
|
createExtension({
|
||||||
|
extensionsDir: userExtensionsDir,
|
||||||
|
name: 'my-theme-extension',
|
||||||
|
themes: [
|
||||||
|
{
|
||||||
|
name: 'My-Awesome-Theme',
|
||||||
|
type: 'custom',
|
||||||
|
text: {
|
||||||
|
primary: '#FF00FF',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await extensionManager.loadExtensions();
|
||||||
|
|
||||||
|
const mockConfig = {
|
||||||
|
getEnableExtensionReloading: () => false,
|
||||||
|
getMcpClientManager: () => ({
|
||||||
|
startExtension: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}),
|
||||||
|
getGeminiClient: () => ({
|
||||||
|
isInitialized: () => false,
|
||||||
|
updateSystemInstruction: vi.fn(),
|
||||||
|
setTools: vi.fn(),
|
||||||
|
}),
|
||||||
|
getHookSystem: () => undefined,
|
||||||
|
getWorkingDir: () => tempHomeDir,
|
||||||
|
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||||
|
getDebugMode: () => false,
|
||||||
|
getFileExclusions: () => ({
|
||||||
|
isIgnored: () => false,
|
||||||
|
}),
|
||||||
|
getGeminiMdFilePaths: () => [],
|
||||||
|
getMcpServers: () => ({}),
|
||||||
|
getAllowedMcpServers: () => [],
|
||||||
|
getSanitizationConfig: () => ({
|
||||||
|
allowedEnvironmentVariables: [],
|
||||||
|
blockedEnvironmentVariables: [],
|
||||||
|
enableEnvironmentVariableRedaction: false,
|
||||||
|
}),
|
||||||
|
getShellExecutionConfig: () => ({
|
||||||
|
terminalWidth: 80,
|
||||||
|
terminalHeight: 24,
|
||||||
|
showColor: false,
|
||||||
|
pager: 'cat',
|
||||||
|
sanitizationConfig: {
|
||||||
|
allowedEnvironmentVariables: [],
|
||||||
|
blockedEnvironmentVariables: [],
|
||||||
|
enableEnvironmentVariableRedaction: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getToolRegistry: () => ({
|
||||||
|
getTools: () => [],
|
||||||
|
}),
|
||||||
|
getProxy: () => undefined,
|
||||||
|
getFileService: () => ({
|
||||||
|
findFiles: async () => [],
|
||||||
|
}),
|
||||||
|
getExtensionLoader: () => ({
|
||||||
|
getExtensions: () => [],
|
||||||
|
}),
|
||||||
|
isTrustedFolder: () => true,
|
||||||
|
getImportFormat: () => 'tree',
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
await extensionManager.start(mockConfig);
|
||||||
|
|
||||||
|
expect(registerSpy).toHaveBeenCalledWith('my-theme-extension', [
|
||||||
|
{
|
||||||
|
name: 'My-Awesome-Theme',
|
||||||
|
type: 'custom',
|
||||||
|
text: {
|
||||||
|
primary: '#FF00FF',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should revert to default theme when extension is stopped', async () => {
|
||||||
|
const extensionName = 'my-theme-extension';
|
||||||
|
const themeName = 'My-Awesome-Theme';
|
||||||
|
const namespacedThemeName = `${themeName} (${extensionName})`;
|
||||||
|
|
||||||
|
createExtension({
|
||||||
|
extensionsDir: userExtensionsDir,
|
||||||
|
name: extensionName,
|
||||||
|
themes: [
|
||||||
|
{
|
||||||
|
name: themeName,
|
||||||
|
type: 'custom',
|
||||||
|
text: {
|
||||||
|
primary: '#FF00FF',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await extensionManager.loadExtensions();
|
||||||
|
|
||||||
|
const mockConfig = {
|
||||||
|
getWorkingDir: () => tempHomeDir,
|
||||||
|
shouldLoadMemoryFromIncludeDirectories: () => false,
|
||||||
|
getWorkspaceContext: () => ({
|
||||||
|
getDirectories: () => [],
|
||||||
|
}),
|
||||||
|
getDebugMode: () => false,
|
||||||
|
getFileService: () => ({
|
||||||
|
findFiles: async () => [],
|
||||||
|
}),
|
||||||
|
getExtensionLoader: () => ({
|
||||||
|
getExtensions: () => [],
|
||||||
|
}),
|
||||||
|
isTrustedFolder: () => true,
|
||||||
|
getImportFormat: () => 'tree',
|
||||||
|
getFileFilteringOptions: () => ({
|
||||||
|
respectGitIgnore: true,
|
||||||
|
respectGeminiIgnore: true,
|
||||||
|
}),
|
||||||
|
getDiscoveryMaxDirs: () => 200,
|
||||||
|
getMcpClientManager: () => ({
|
||||||
|
getMcpInstructions: () => '',
|
||||||
|
startExtension: vi.fn().mockResolvedValue(undefined),
|
||||||
|
stopExtension: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}),
|
||||||
|
setUserMemory: vi.fn(),
|
||||||
|
setGeminiMdFileCount: vi.fn(),
|
||||||
|
setGeminiMdFilePaths: vi.fn(),
|
||||||
|
getEnableExtensionReloading: () => true,
|
||||||
|
getGeminiClient: () => ({
|
||||||
|
isInitialized: () => false,
|
||||||
|
updateSystemInstruction: vi.fn(),
|
||||||
|
setTools: vi.fn(),
|
||||||
|
}),
|
||||||
|
getHookSystem: () => undefined,
|
||||||
|
getProxy: () => undefined,
|
||||||
|
getAgentRegistry: () => ({
|
||||||
|
reload: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
await extensionManager.start(mockConfig);
|
||||||
|
|
||||||
|
// Set the active theme to the one from the extension
|
||||||
|
themeManager.setActiveTheme(namespacedThemeName);
|
||||||
|
expect(themeManager.getActiveTheme().name).toBe(namespacedThemeName);
|
||||||
|
|
||||||
|
// Stop the extension
|
||||||
|
await extensionManager.disableExtension(extensionName, SettingScope.User);
|
||||||
|
|
||||||
|
// Check that the active theme has reverted to the default
|
||||||
|
expect(themeManager.getActiveTheme().name).toBe(DEFAULT_THEME.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -69,6 +69,7 @@ import {
|
|||||||
ExtensionSettingScope,
|
ExtensionSettingScope,
|
||||||
} from './extensions/extensionSettings.js';
|
} from './extensions/extensionSettings.js';
|
||||||
import type { EventEmitter } from 'node:stream';
|
import type { EventEmitter } from 'node:stream';
|
||||||
|
import { themeManager } from '../ui/themes/theme-manager.js';
|
||||||
|
|
||||||
interface ExtensionManagerParams {
|
interface ExtensionManagerParams {
|
||||||
enabledExtensionOverrides?: string[];
|
enabledExtensionOverrides?: string[];
|
||||||
@@ -468,6 +469,20 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async startExtension(extension: GeminiCLIExtension) {
|
||||||
|
await super.startExtension(extension);
|
||||||
|
if (extension.themes) {
|
||||||
|
themeManager.registerExtensionThemes(extension.name, extension.themes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async stopExtension(extension: GeminiCLIExtension) {
|
||||||
|
await super.stopExtension(extension);
|
||||||
|
if (extension.themes) {
|
||||||
|
themeManager.unregisterExtensionThemes(extension.name, extension.themes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads all installed extensions, should only be called once.
|
* Loads all installed extensions, should only be called once.
|
||||||
*/
|
*/
|
||||||
@@ -698,6 +713,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
resolvedSettings,
|
resolvedSettings,
|
||||||
skills,
|
skills,
|
||||||
agents: agentLoadResult.agents,
|
agents: agentLoadResult.agents,
|
||||||
|
themes: config.themes,
|
||||||
};
|
};
|
||||||
this.loadedExtensions = [...this.loadedExtensions, extension];
|
this.loadedExtensions = [...this.loadedExtensions, extension];
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import type {
|
import type {
|
||||||
MCPServerConfig,
|
MCPServerConfig,
|
||||||
ExtensionInstallMetadata,
|
ExtensionInstallMetadata,
|
||||||
|
CustomTheme,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
@@ -27,6 +28,11 @@ export interface ExtensionConfig {
|
|||||||
contextFileName?: string | string[];
|
contextFileName?: string | string[];
|
||||||
excludeTools?: string[];
|
excludeTools?: string[];
|
||||||
settings?: ExtensionSetting[];
|
settings?: ExtensionSetting[];
|
||||||
|
/**
|
||||||
|
* Custom themes contributed by this extension.
|
||||||
|
* These themes will be registered when the extension is activated.
|
||||||
|
*/
|
||||||
|
themes?: CustomTheme[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionUpdateInfo {
|
export interface ExtensionUpdateInfo {
|
||||||
|
|||||||
@@ -9,19 +9,17 @@
|
|||||||
// to regenerate the settings reference in `docs/get-started/configuration.md`.
|
// to regenerate the settings reference in `docs/get-started/configuration.md`.
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
import type {
|
|
||||||
MCPServerConfig,
|
|
||||||
BugCommandSettings,
|
|
||||||
TelemetrySettings,
|
|
||||||
AuthType,
|
|
||||||
AgentOverride,
|
|
||||||
} from '@google/gemini-cli-core';
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||||
DEFAULT_MODEL_CONFIGS,
|
DEFAULT_MODEL_CONFIGS,
|
||||||
|
type MCPServerConfig,
|
||||||
|
type BugCommandSettings,
|
||||||
|
type TelemetrySettings,
|
||||||
|
type AuthType,
|
||||||
|
type AgentOverride,
|
||||||
|
type CustomTheme,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { CustomTheme } from '../ui/themes/theme.js';
|
|
||||||
import type { SessionRetentionSettings } from './settings.js';
|
import type { SessionRetentionSettings } from './settings.js';
|
||||||
import { DEFAULT_MIN_RETENTION } from '../utils/sessionCleanup.js';
|
import { DEFAULT_MIN_RETENTION } from '../utils/sessionCleanup.js';
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import * as path from 'node:path';
|
|||||||
import {
|
import {
|
||||||
type MCPServerConfig,
|
type MCPServerConfig,
|
||||||
type ExtensionInstallMetadata,
|
type ExtensionInstallMetadata,
|
||||||
|
type ExtensionSetting,
|
||||||
|
type CustomTheme,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
EXTENSIONS_CONFIG_FILENAME,
|
EXTENSIONS_CONFIG_FILENAME,
|
||||||
INSTALL_METADATA_FILENAME,
|
INSTALL_METADATA_FILENAME,
|
||||||
} from '../config/extensions/variables.js';
|
} from '../config/extensions/variables.js';
|
||||||
import type { ExtensionSetting } from '../config/extensions/extensionSettings.js';
|
|
||||||
|
|
||||||
export function createExtension({
|
export function createExtension({
|
||||||
extensionsDir = 'extensions-dir',
|
extensionsDir = 'extensions-dir',
|
||||||
@@ -25,12 +26,20 @@ export function createExtension({
|
|||||||
mcpServers = {} as Record<string, MCPServerConfig>,
|
mcpServers = {} as Record<string, MCPServerConfig>,
|
||||||
installMetadata = undefined as ExtensionInstallMetadata | undefined,
|
installMetadata = undefined as ExtensionInstallMetadata | undefined,
|
||||||
settings = undefined as ExtensionSetting[] | undefined,
|
settings = undefined as ExtensionSetting[] | undefined,
|
||||||
|
themes = undefined as CustomTheme[] | undefined,
|
||||||
} = {}): string {
|
} = {}): string {
|
||||||
const extDir = path.join(extensionsDir, name);
|
const extDir = path.join(extensionsDir, name);
|
||||||
fs.mkdirSync(extDir, { recursive: true });
|
fs.mkdirSync(extDir, { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
|
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
|
||||||
JSON.stringify({ name, version, contextFileName, mcpServers, settings }),
|
JSON.stringify({
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
contextFileName,
|
||||||
|
mcpServers,
|
||||||
|
settings,
|
||||||
|
themes,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (addContextFile) {
|
if (addContextFile) {
|
||||||
|
|||||||
@@ -109,15 +109,6 @@ export function ThemeDialog({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate theme items filtered by selected scope
|
|
||||||
const customThemes =
|
|
||||||
selectedScope === SettingScope.User
|
|
||||||
? settings.user.settings.ui?.customThemes || {}
|
|
||||||
: settings.merged.ui.customThemes;
|
|
||||||
const builtInThemes = themeManager
|
|
||||||
.getAvailableThemes()
|
|
||||||
.filter((theme) => theme.type !== 'custom');
|
|
||||||
const customThemeNames = Object.keys(customThemes);
|
|
||||||
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
|
|
||||||
const terminalThemeType = getThemeTypeFromBackgroundColor(
|
const terminalThemeType = getThemeTypeFromBackgroundColor(
|
||||||
@@ -125,8 +116,9 @@ export function ThemeDialog({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Generate theme items
|
// Generate theme items
|
||||||
const themeItems = [
|
const themeItems = themeManager
|
||||||
...builtInThemes.map((theme) => {
|
.getAvailableThemes()
|
||||||
|
.map((theme) => {
|
||||||
const fullTheme = themeManager.getTheme(theme.name);
|
const fullTheme = themeManager.getTheme(theme.name);
|
||||||
const themeBackground = fullTheme
|
const themeBackground = fullTheme
|
||||||
? resolveColor(fullTheme.colors.Background)
|
? resolveColor(fullTheme.colors.Background)
|
||||||
@@ -140,22 +132,8 @@ export function ThemeDialog({
|
|||||||
terminalBackgroundColor,
|
terminalBackgroundColor,
|
||||||
terminalThemeType,
|
terminalThemeType,
|
||||||
);
|
);
|
||||||
}),
|
})
|
||||||
...customThemeNames.map((name) => {
|
.sort((a, b) => {
|
||||||
const themeConfig = customThemes[name];
|
|
||||||
const bg = themeConfig.background?.primary ?? themeConfig.Background;
|
|
||||||
const themeBackground = bg ? resolveColor(bg) : undefined;
|
|
||||||
|
|
||||||
return generateThemeItem(
|
|
||||||
name,
|
|
||||||
'Custom',
|
|
||||||
'custom',
|
|
||||||
themeBackground,
|
|
||||||
terminalBackgroundColor,
|
|
||||||
terminalThemeType,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
].sort((a, b) => {
|
|
||||||
// Show compatible themes first
|
// Show compatible themes first
|
||||||
if (a.isCompatible && !b.isCompatible) return -1;
|
if (a.isCompatible && !b.isCompatible) return -1;
|
||||||
if (!a.isCompatible && b.isCompatible) return 1;
|
if (!a.isCompatible && b.isCompatible) return 1;
|
||||||
@@ -314,9 +292,20 @@ export function ThemeDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (item.themeNameDisplay && item.themeTypeDisplay) {
|
if (item.themeNameDisplay && item.themeTypeDisplay) {
|
||||||
|
const match = item.themeNameDisplay.match(/^(.*) \((.*)\)$/);
|
||||||
|
let themeNamePart: React.ReactNode = item.themeNameDisplay;
|
||||||
|
if (match) {
|
||||||
|
themeNamePart = (
|
||||||
|
<>
|
||||||
|
{match[1]}{' '}
|
||||||
|
<Text color={theme.text.secondary}>({match[2]})</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text color={titleColor} wrap="truncate" key={item.key}>
|
<Text color={titleColor} wrap="truncate" key={item.key}>
|
||||||
{item.themeNameDisplay}{' '}
|
{themeNamePart}{' '}
|
||||||
<Text color={theme.text.secondary}>
|
<Text color={theme.text.secondary}>
|
||||||
{item.themeTypeDisplay}
|
{item.themeTypeDisplay}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ if (process.env['NO_COLOR'] !== undefined) {
|
|||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { themeManager, DEFAULT_THEME } from './theme-manager.js';
|
import { themeManager, DEFAULT_THEME } from './theme-manager.js';
|
||||||
import type { CustomTheme } from './theme.js';
|
import type { CustomTheme } from '@google/gemini-cli-core';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import type * as osActual from 'node:os';
|
import type * as osActual from 'node:os';
|
||||||
@@ -188,4 +188,54 @@ describe('ThemeManager', () => {
|
|||||||
consoleWarnSpy.mockRestore();
|
consoleWarnSpy.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('extension themes', () => {
|
||||||
|
it('should register and unregister themes from extensions with namespacing', () => {
|
||||||
|
const extTheme: CustomTheme = {
|
||||||
|
...validCustomTheme,
|
||||||
|
name: 'ExtensionTheme',
|
||||||
|
};
|
||||||
|
const extensionName = 'test-extension';
|
||||||
|
const namespacedName = `ExtensionTheme (${extensionName})`;
|
||||||
|
|
||||||
|
themeManager.registerExtensionThemes(extensionName, [extTheme]);
|
||||||
|
expect(themeManager.getCustomThemeNames()).toContain(namespacedName);
|
||||||
|
expect(themeManager.isCustomTheme(namespacedName)).toBe(true);
|
||||||
|
|
||||||
|
themeManager.unregisterExtensionThemes(extensionName, [extTheme]);
|
||||||
|
expect(themeManager.getCustomThemeNames()).not.toContain(namespacedName);
|
||||||
|
expect(themeManager.isCustomTheme(namespacedName)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow extension themes to overwrite built-in themes even with prefixing', () => {
|
||||||
|
// availableThemes has 'Ayu'.
|
||||||
|
// We verify that it DOES prefix, so it won't collide even if extension name is similar.
|
||||||
|
themeManager.registerExtensionThemes('Ext', [
|
||||||
|
{ ...validCustomTheme, name: 'Theme' },
|
||||||
|
]);
|
||||||
|
expect(themeManager.getCustomThemeNames()).toContain('Theme (Ext)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow extension themes and settings themes to coexist', () => {
|
||||||
|
const extTheme: CustomTheme = {
|
||||||
|
...validCustomTheme,
|
||||||
|
name: 'ExtensionTheme',
|
||||||
|
};
|
||||||
|
const settingsTheme: CustomTheme = {
|
||||||
|
...validCustomTheme,
|
||||||
|
name: 'SettingsTheme',
|
||||||
|
};
|
||||||
|
|
||||||
|
themeManager.registerExtensionThemes('Ext', [extTheme]);
|
||||||
|
themeManager.loadCustomThemes({ SettingsTheme: settingsTheme });
|
||||||
|
|
||||||
|
expect(themeManager.getCustomThemeNames()).toContain(
|
||||||
|
'ExtensionTheme (Ext)',
|
||||||
|
);
|
||||||
|
expect(themeManager.getCustomThemeNames()).toContain('SettingsTheme');
|
||||||
|
|
||||||
|
expect(themeManager.isCustomTheme('ExtensionTheme (Ext)')).toBe(true);
|
||||||
|
expect(themeManager.isCustomTheme('SettingsTheme')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import { ShadesOfPurple } from './shades-of-purple.js';
|
|||||||
import { XCode } from './xcode.js';
|
import { XCode } from './xcode.js';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import type { Theme, ThemeType, CustomTheme } from './theme.js';
|
import type { Theme, ThemeType } from './theme.js';
|
||||||
|
import type { CustomTheme } from '@google/gemini-cli-core';
|
||||||
import { createCustomTheme, validateCustomTheme } from './theme.js';
|
import { createCustomTheme, validateCustomTheme } from './theme.js';
|
||||||
import type { SemanticColors } from './semantic-tokens.js';
|
import type { SemanticColors } from './semantic-tokens.js';
|
||||||
import { ANSI } from './ansi.js';
|
import { ANSI } from './ansi.js';
|
||||||
@@ -38,7 +39,9 @@ export const DEFAULT_THEME: Theme = DefaultDark;
|
|||||||
class ThemeManager {
|
class ThemeManager {
|
||||||
private readonly availableThemes: Theme[];
|
private readonly availableThemes: Theme[];
|
||||||
private activeTheme: Theme;
|
private activeTheme: Theme;
|
||||||
private customThemes: Map<string, Theme> = new Map();
|
private settingsThemes: Map<string, Theme> = new Map();
|
||||||
|
private extensionThemes: Map<string, Theme> = new Map();
|
||||||
|
private fileThemes: Map<string, Theme> = new Map();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.availableThemes = [
|
this.availableThemes = [
|
||||||
@@ -65,7 +68,7 @@ class ThemeManager {
|
|||||||
* @param customThemesSettings Custom themes from settings.
|
* @param customThemesSettings Custom themes from settings.
|
||||||
*/
|
*/
|
||||||
loadCustomThemes(customThemesSettings?: Record<string, CustomTheme>): void {
|
loadCustomThemes(customThemesSettings?: Record<string, CustomTheme>): void {
|
||||||
this.customThemes.clear();
|
this.settingsThemes.clear();
|
||||||
|
|
||||||
if (!customThemesSettings) {
|
if (!customThemesSettings) {
|
||||||
return;
|
return;
|
||||||
@@ -88,7 +91,7 @@ class ThemeManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const theme = createCustomTheme(themeWithDefaults);
|
const theme = createCustomTheme(themeWithDefaults);
|
||||||
this.customThemes.set(name, theme);
|
this.settingsThemes.set(name, theme);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLogger.warn(`Failed to load custom theme "${name}":`, error);
|
debugLogger.warn(`Failed to load custom theme "${name}":`, error);
|
||||||
}
|
}
|
||||||
@@ -96,16 +99,103 @@ class ThemeManager {
|
|||||||
debugLogger.warn(`Invalid custom theme "${name}": ${validation.error}`);
|
debugLogger.warn(`Invalid custom theme "${name}": ${validation.error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the current active theme is a custom theme, keep it if still valid
|
// If the current active theme is a settings theme, keep it if still valid
|
||||||
if (
|
if (
|
||||||
this.activeTheme &&
|
this.activeTheme &&
|
||||||
this.activeTheme.type === 'custom' &&
|
this.activeTheme.type === 'custom' &&
|
||||||
this.customThemes.has(this.activeTheme.name)
|
this.settingsThemes.has(this.activeTheme.name)
|
||||||
) {
|
) {
|
||||||
this.activeTheme = this.customThemes.get(this.activeTheme.name)!;
|
this.activeTheme = this.settingsThemes.get(this.activeTheme.name)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads custom themes from extensions.
|
||||||
|
* @param extensionName The name of the extension providing the themes.
|
||||||
|
* @param customThemes Custom themes from extensions.
|
||||||
|
*/
|
||||||
|
registerExtensionThemes(
|
||||||
|
extensionName: string,
|
||||||
|
customThemes?: CustomTheme[],
|
||||||
|
): void {
|
||||||
|
if (!customThemes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLogger.log(
|
||||||
|
`Registering extension themes for "${extensionName}":`,
|
||||||
|
customThemes,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const customThemeConfig of customThemes) {
|
||||||
|
const namespacedName = `${customThemeConfig.name} (${extensionName})`;
|
||||||
|
|
||||||
|
// Check for collisions with built-in themes (unlikely with prefix, but safe)
|
||||||
|
if (this.availableThemes.some((t) => t.name === namespacedName)) {
|
||||||
|
debugLogger.warn(
|
||||||
|
`Theme name collision: "${namespacedName}" is a built-in theme. Skipping.`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = validateCustomTheme(customThemeConfig);
|
||||||
|
if (validation.isValid) {
|
||||||
|
if (validation.warning) {
|
||||||
|
debugLogger.warn(`Theme "${namespacedName}": ${validation.warning}`);
|
||||||
|
}
|
||||||
|
const themeWithDefaults: CustomTheme = {
|
||||||
|
...DEFAULT_THEME.colors,
|
||||||
|
...customThemeConfig,
|
||||||
|
name: namespacedName,
|
||||||
|
type: 'custom',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const theme = createCustomTheme(themeWithDefaults);
|
||||||
|
this.extensionThemes.set(namespacedName, theme);
|
||||||
|
debugLogger.log(`Registered theme: ${namespacedName}`);
|
||||||
|
} catch (error) {
|
||||||
|
debugLogger.warn(
|
||||||
|
`Failed to load custom theme "${namespacedName}":`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLogger.warn(
|
||||||
|
`Invalid custom theme "${namespacedName}": ${validation.error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters custom themes from extensions.
|
||||||
|
* @param extensionName The name of the extension.
|
||||||
|
* @param customThemes Custom themes to unregister.
|
||||||
|
*/
|
||||||
|
unregisterExtensionThemes(
|
||||||
|
extensionName: string,
|
||||||
|
customThemes?: CustomTheme[],
|
||||||
|
): void {
|
||||||
|
if (!customThemes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const theme of customThemes) {
|
||||||
|
const namespacedName = `${theme.name} (${extensionName})`;
|
||||||
|
this.extensionThemes.delete(namespacedName);
|
||||||
|
debugLogger.log(`Unregistered theme: ${namespacedName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all registered extension themes.
|
||||||
|
* This is primarily for testing purposes to reset state between tests.
|
||||||
|
*/
|
||||||
|
clearExtensionThemes(): void {
|
||||||
|
this.extensionThemes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the active theme.
|
* Sets the active theme.
|
||||||
* @param themeName The name of the theme to set as active.
|
* @param themeName The name of the theme to set as active.
|
||||||
@@ -133,13 +223,23 @@ class ThemeManager {
|
|||||||
const isBuiltIn = this.availableThemes.some(
|
const isBuiltIn = this.availableThemes.some(
|
||||||
(t) => t.name === this.activeTheme.name,
|
(t) => t.name === this.activeTheme.name,
|
||||||
);
|
);
|
||||||
const isCustom = [...this.customThemes.values()].includes(
|
const isCustom =
|
||||||
this.activeTheme,
|
[...this.settingsThemes.values()].includes(this.activeTheme) ||
|
||||||
);
|
[...this.extensionThemes.values()].includes(this.activeTheme) ||
|
||||||
|
[...this.fileThemes.values()].includes(this.activeTheme);
|
||||||
|
|
||||||
if (isBuiltIn || isCustom) {
|
if (isBuiltIn || isCustom) {
|
||||||
return this.activeTheme;
|
return this.activeTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the theme object is no longer valid, try to find it again by name.
|
||||||
|
// This handles the case where extensions are reloaded and theme objects
|
||||||
|
// are re-created.
|
||||||
|
const reloadedTheme = this.findThemeByName(this.activeTheme.name);
|
||||||
|
if (reloadedTheme) {
|
||||||
|
this.activeTheme = reloadedTheme;
|
||||||
|
return this.activeTheme;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to default if no active theme or if it's no longer valid.
|
// Fallback to default if no active theme or if it's no longer valid.
|
||||||
@@ -155,12 +255,20 @@ class ThemeManager {
|
|||||||
return this.getActiveTheme().semanticColors;
|
return this.getActiveTheme().semanticColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getAllCustomThemes(): Theme[] {
|
||||||
|
return [
|
||||||
|
...Array.from(this.settingsThemes.values()),
|
||||||
|
...Array.from(this.extensionThemes.values()),
|
||||||
|
...Array.from(this.fileThemes.values()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a list of custom theme names.
|
* Gets a list of custom theme names.
|
||||||
* @returns Array of custom theme names.
|
* @returns Array of custom theme names.
|
||||||
*/
|
*/
|
||||||
getCustomThemeNames(): string[] {
|
getCustomThemeNames(): string[] {
|
||||||
return Array.from(this.customThemes.keys());
|
return this._getAllCustomThemes().map((theme) => theme.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,7 +277,11 @@ class ThemeManager {
|
|||||||
* @returns True if the theme is custom.
|
* @returns True if the theme is custom.
|
||||||
*/
|
*/
|
||||||
isCustomTheme(themeName: string): boolean {
|
isCustomTheme(themeName: string): boolean {
|
||||||
return this.customThemes.has(themeName);
|
return (
|
||||||
|
this.settingsThemes.has(themeName) ||
|
||||||
|
this.extensionThemes.has(themeName) ||
|
||||||
|
this.fileThemes.has(themeName)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,13 +294,11 @@ class ThemeManager {
|
|||||||
isCustom: false,
|
isCustom: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const customThemes = Array.from(this.customThemes.values()).map(
|
const customThemes = this._getAllCustomThemes().map((theme) => ({
|
||||||
(theme) => ({
|
|
||||||
name: theme.name,
|
name: theme.name,
|
||||||
type: theme.type,
|
type: theme.type,
|
||||||
isCustom: true,
|
isCustom: true,
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
const allThemes = [...builtInThemes, ...customThemes];
|
const allThemes = [...builtInThemes, ...customThemes];
|
||||||
|
|
||||||
@@ -232,7 +342,7 @@ class ThemeManager {
|
|||||||
* @returns A list of all available themes.
|
* @returns A list of all available themes.
|
||||||
*/
|
*/
|
||||||
getAllThemes(): Theme[] {
|
getAllThemes(): Theme[] {
|
||||||
return [...this.availableThemes, ...Array.from(this.customThemes.values())];
|
return [...this.availableThemes, ...this._getAllCustomThemes()];
|
||||||
}
|
}
|
||||||
|
|
||||||
private isPath(themeName: string): boolean {
|
private isPath(themeName: string): boolean {
|
||||||
@@ -249,8 +359,8 @@ class ThemeManager {
|
|||||||
const canonicalPath = fs.realpathSync(path.resolve(themePath));
|
const canonicalPath = fs.realpathSync(path.resolve(themePath));
|
||||||
|
|
||||||
// 1. Check cache using the canonical path.
|
// 1. Check cache using the canonical path.
|
||||||
if (this.customThemes.has(canonicalPath)) {
|
if (this.fileThemes.has(canonicalPath)) {
|
||||||
return this.customThemes.get(canonicalPath);
|
return this.fileThemes.get(canonicalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Perform security check.
|
// 2. Perform security check.
|
||||||
@@ -288,7 +398,7 @@ class ThemeManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const theme = createCustomTheme(themeWithDefaults);
|
const theme = createCustomTheme(themeWithDefaults);
|
||||||
this.customThemes.set(canonicalPath, theme); // Cache by canonical path
|
this.fileThemes.set(canonicalPath, theme); // Cache by canonical path
|
||||||
return theme;
|
return theme;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Any error in the process (file not found, bad JSON, etc.) is caught here.
|
// Any error in the process (file not found, bad JSON, etc.) is caught here.
|
||||||
@@ -318,13 +428,21 @@ class ThemeManager {
|
|||||||
return builtInTheme;
|
return builtInTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check custom themes that have been loaded from settings, or file paths
|
// Then check custom themes that have been loaded from settings, extensions, or file paths
|
||||||
if (this.isPath(themeName)) {
|
if (this.isPath(themeName)) {
|
||||||
return this.loadThemeFromFile(themeName);
|
return this.loadThemeFromFile(themeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.customThemes.has(themeName)) {
|
if (this.settingsThemes.has(themeName)) {
|
||||||
return this.customThemes.get(themeName);
|
return this.settingsThemes.get(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.extensionThemes.has(themeName)) {
|
||||||
|
return this.extensionThemes.get(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fileThemes.has(themeName)) {
|
||||||
|
return this.fileThemes.get(themeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's not a built-in, not in cache, and not a valid file path,
|
// If it's not a built-in, not in cache, and not a valid file path,
|
||||||
|
|||||||
@@ -5,11 +5,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import * as themeModule from './theme.js';
|
import {
|
||||||
|
createCustomTheme,
|
||||||
|
validateCustomTheme,
|
||||||
|
pickDefaultThemeName,
|
||||||
|
darkTheme,
|
||||||
|
type Theme,
|
||||||
|
} from './theme.js';
|
||||||
import { themeManager } from './theme-manager.js';
|
import { themeManager } from './theme-manager.js';
|
||||||
|
import type { CustomTheme } from '@google/gemini-cli-core';
|
||||||
const { validateCustomTheme, createCustomTheme } = themeModule;
|
|
||||||
type CustomTheme = themeModule.CustomTheme;
|
|
||||||
|
|
||||||
describe('createCustomTheme', () => {
|
describe('createCustomTheme', () => {
|
||||||
const baseTheme: CustomTheme = {
|
const baseTheme: CustomTheme = {
|
||||||
@@ -152,7 +156,6 @@ describe('themeManager.loadCustomThemes', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('should use values from DEFAULT_THEME when DiffAdded and DiffRemoved are not provided', () => {
|
it('should use values from DEFAULT_THEME when DiffAdded and DiffRemoved are not provided', () => {
|
||||||
const { darkTheme } = themeModule;
|
|
||||||
const legacyTheme: Partial<CustomTheme> = { ...baseTheme };
|
const legacyTheme: Partial<CustomTheme> = { ...baseTheme };
|
||||||
delete legacyTheme.DiffAdded;
|
delete legacyTheme.DiffAdded;
|
||||||
delete legacyTheme.DiffRemoved;
|
delete legacyTheme.DiffRemoved;
|
||||||
@@ -170,12 +173,11 @@ describe('themeManager.loadCustomThemes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('pickDefaultThemeName', () => {
|
describe('pickDefaultThemeName', () => {
|
||||||
const { pickDefaultThemeName } = themeModule;
|
|
||||||
const mockThemes = [
|
const mockThemes = [
|
||||||
{ name: 'Dark Theme', type: 'dark', colors: { Background: '#000000' } },
|
{ name: 'Dark Theme', type: 'dark', colors: { Background: '#000000' } },
|
||||||
{ name: 'Light Theme', type: 'light', colors: { Background: '#ffffff' } },
|
{ name: 'Light Theme', type: 'light', colors: { Background: '#ffffff' } },
|
||||||
{ name: 'Blue Theme', type: 'dark', colors: { Background: '#0000ff' } },
|
{ name: 'Blue Theme', type: 'dark', colors: { Background: '#0000ff' } },
|
||||||
] as unknown as themeModule.Theme[];
|
] as unknown as Theme[];
|
||||||
|
|
||||||
it('should return exact match if found', () => {
|
it('should return exact match if found', () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -5,13 +5,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
import type { SemanticColors } from './semantic-tokens.js';
|
import type { SemanticColors } from './semantic-tokens.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resolveColor,
|
resolveColor,
|
||||||
interpolateColor,
|
interpolateColor,
|
||||||
getThemeTypeFromBackgroundColor,
|
getThemeTypeFromBackgroundColor,
|
||||||
} from './color-utils.js';
|
} from './color-utils.js';
|
||||||
|
|
||||||
|
import type { CustomTheme } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
|
export type { CustomTheme };
|
||||||
|
|
||||||
export type ThemeType = 'light' | 'dark' | 'ansi' | 'custom';
|
export type ThemeType = 'light' | 'dark' | 'ansi' | 'custom';
|
||||||
|
|
||||||
export interface ColorsTheme {
|
export interface ColorsTheme {
|
||||||
@@ -33,57 +39,6 @@ export interface ColorsTheme {
|
|||||||
GradientColors?: string[];
|
GradientColors?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomTheme {
|
|
||||||
type: 'custom';
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
text?: {
|
|
||||||
primary?: string;
|
|
||||||
secondary?: string;
|
|
||||||
link?: string;
|
|
||||||
accent?: string;
|
|
||||||
response?: string;
|
|
||||||
};
|
|
||||||
background?: {
|
|
||||||
primary?: string;
|
|
||||||
diff?: {
|
|
||||||
added?: string;
|
|
||||||
removed?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
border?: {
|
|
||||||
default?: string;
|
|
||||||
focused?: string;
|
|
||||||
};
|
|
||||||
ui?: {
|
|
||||||
comment?: string;
|
|
||||||
symbol?: string;
|
|
||||||
gradient?: string[];
|
|
||||||
};
|
|
||||||
status?: {
|
|
||||||
error?: string;
|
|
||||||
success?: string;
|
|
||||||
warning?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Legacy properties (all optional)
|
|
||||||
Background?: string;
|
|
||||||
Foreground?: string;
|
|
||||||
LightBlue?: string;
|
|
||||||
AccentBlue?: string;
|
|
||||||
AccentPurple?: string;
|
|
||||||
AccentCyan?: string;
|
|
||||||
AccentGreen?: string;
|
|
||||||
AccentYellow?: string;
|
|
||||||
AccentRed?: string;
|
|
||||||
DiffAdded?: string;
|
|
||||||
DiffRemoved?: string;
|
|
||||||
Comment?: string;
|
|
||||||
Gray?: string;
|
|
||||||
DarkGray?: string;
|
|
||||||
GradientColors?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const lightTheme: ColorsTheme = {
|
export const lightTheme: ColorsTheme = {
|
||||||
type: 'light',
|
type: 'light',
|
||||||
Background: '#FAFAFA',
|
Background: '#FAFAFA',
|
||||||
|
|||||||
@@ -178,6 +178,57 @@ export interface AgentSettings {
|
|||||||
overrides?: Record<string, AgentOverride>;
|
overrides?: Record<string, AgentOverride>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CustomTheme {
|
||||||
|
type: 'custom';
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
text?: {
|
||||||
|
primary?: string;
|
||||||
|
secondary?: string;
|
||||||
|
link?: string;
|
||||||
|
accent?: string;
|
||||||
|
response?: string;
|
||||||
|
};
|
||||||
|
background?: {
|
||||||
|
primary?: string;
|
||||||
|
diff?: {
|
||||||
|
added?: string;
|
||||||
|
removed?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
border?: {
|
||||||
|
default?: string;
|
||||||
|
focused?: string;
|
||||||
|
};
|
||||||
|
ui?: {
|
||||||
|
comment?: string;
|
||||||
|
symbol?: string;
|
||||||
|
gradient?: string[];
|
||||||
|
};
|
||||||
|
status?: {
|
||||||
|
error?: string;
|
||||||
|
success?: string;
|
||||||
|
warning?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Legacy properties (all optional)
|
||||||
|
Background?: string;
|
||||||
|
Foreground?: string;
|
||||||
|
LightBlue?: string;
|
||||||
|
AccentBlue?: string;
|
||||||
|
AccentPurple?: string;
|
||||||
|
AccentCyan?: string;
|
||||||
|
AccentGreen?: string;
|
||||||
|
AccentYellow?: string;
|
||||||
|
AccentRed?: string;
|
||||||
|
DiffAdded?: string;
|
||||||
|
DiffRemoved?: string;
|
||||||
|
Comment?: string;
|
||||||
|
Gray?: string;
|
||||||
|
DarkGray?: string;
|
||||||
|
GradientColors?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All information required in CLI to handle an extension. Defined in Core so
|
* All information required in CLI to handle an extension. Defined in Core so
|
||||||
* that the collection of loaded, active, and inactive extensions can be passed
|
* that the collection of loaded, active, and inactive extensions can be passed
|
||||||
@@ -199,6 +250,11 @@ export interface GeminiCLIExtension {
|
|||||||
resolvedSettings?: ResolvedExtensionSetting[];
|
resolvedSettings?: ResolvedExtensionSetting[];
|
||||||
skills?: SkillDefinition[];
|
skills?: SkillDefinition[];
|
||||||
agents?: AgentDefinition[];
|
agents?: AgentDefinition[];
|
||||||
|
/**
|
||||||
|
* Custom themes contributed by this extension.
|
||||||
|
* These themes will be registered when the extension is activated.
|
||||||
|
*/
|
||||||
|
themes?: CustomTheme[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionInstallMetadata {
|
export interface ExtensionInstallMetadata {
|
||||||
|
|||||||
Reference in New Issue
Block a user