mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-30 15:04:16 -07:00
feat(core): implement feature lifecycle management (Alpha, Beta, GA)
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
configureSpecificSetting,
|
||||
getExtensionManager,
|
||||
} from './utils.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { loadSettings, isFeatureEnabled } from '../../config/settings.js';
|
||||
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
@@ -45,10 +45,10 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
|
||||
const { name, setting, scope } = args;
|
||||
const settings = loadSettings(process.cwd()).merged;
|
||||
|
||||
if (!(settings.experimental?.extensionConfig ?? true)) {
|
||||
if (!isFeatureEnabled(settings, 'extensionConfig')) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'Extension configuration is currently disabled. Enable it by setting "experimental.extensionConfig" to true.',
|
||||
'Extension configuration is currently disabled. Enable it by setting "features.extensionConfig" to true.',
|
||||
);
|
||||
await exitCli();
|
||||
return;
|
||||
|
||||
@@ -2647,6 +2647,62 @@ describe('loadCliConfig approval mode', () => {
|
||||
expect(plansDir).toContain('.custom-plans');
|
||||
});
|
||||
|
||||
describe('Feature Gates', () => {
|
||||
it('should parse --feature-gates CLI flag', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
'--feature-gates',
|
||||
'plan=true,enableAgents=false',
|
||||
];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
expect(argv.featureGates).toBe('plan=true,enableAgents=false');
|
||||
});
|
||||
|
||||
it('should respect "features" setting in loadCliConfig', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
features: { plan: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.isFeatureEnabled('plan')).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize --feature-gates flag over settings', async () => {
|
||||
process.argv = ['node', 'script.js', '--feature-gates', 'plan=true'];
|
||||
const settings = createTestMergedSettings({
|
||||
features: { plan: false },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.isFeatureEnabled('plan')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow plan approval mode when features.plan is enabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
|
||||
const settings = createTestMergedSettings({
|
||||
features: { plan: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
|
||||
});
|
||||
|
||||
it('should throw error when --approval-mode=plan is used but features.plan is disabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
|
||||
const settings = createTestMergedSettings({
|
||||
features: { plan: false },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
await expect(
|
||||
loadCliConfig(settings, 'test-session', argv),
|
||||
).rejects.toThrow(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Untrusted Folder Scenarios ---
|
||||
describe('when folder is NOT trusted', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
type MergedSettings,
|
||||
saveModelChange,
|
||||
loadSettings,
|
||||
isFeatureEnabled,
|
||||
} from './settings.js';
|
||||
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
@@ -78,6 +79,7 @@ export interface CliArgs {
|
||||
allowedTools: string[] | undefined;
|
||||
acp?: boolean;
|
||||
experimentalAcp?: boolean;
|
||||
featureGates?: string | undefined;
|
||||
extensions: string[] | undefined;
|
||||
listExtensions: boolean | undefined;
|
||||
resume: string | typeof RESUME_LATEST | undefined;
|
||||
@@ -182,6 +184,12 @@ export async function parseArguments(
|
||||
description:
|
||||
'Starts the agent in ACP mode (deprecated, use --acp instead)',
|
||||
})
|
||||
.option('feature-gates', {
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
description:
|
||||
'Comma-separated list of feature key-value pairs (e.g. "Foo=true,Bar=false")',
|
||||
})
|
||||
.option('allowed-mcp-server-names', {
|
||||
type: 'array',
|
||||
string: true,
|
||||
@@ -328,7 +336,7 @@ export async function parseArguments(
|
||||
return true;
|
||||
});
|
||||
|
||||
if (settings.experimental?.extensionManagement) {
|
||||
if (isFeatureEnabled(settings, 'extensionManagement')) {
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
@@ -486,7 +494,7 @@ export async function loadCliConfig(
|
||||
.getExtensions()
|
||||
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
|
||||
|
||||
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
||||
const experimentalJitContext = isFeatureEnabled(settings, 'jitContext');
|
||||
|
||||
let memoryContent: string | HierarchicalMemory = '';
|
||||
let fileCount = 0;
|
||||
@@ -532,7 +540,7 @@ export async function loadCliConfig(
|
||||
approvalMode = ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case 'plan':
|
||||
if (!(settings.experimental?.plan ?? false)) {
|
||||
if (!isFeatureEnabled(settings, 'plan')) {
|
||||
debugLogger.warn(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
|
||||
);
|
||||
@@ -759,15 +767,17 @@ export async function loadCliConfig(
|
||||
bugCommand: settings.advanced?.bugCommand,
|
||||
model: resolvedModel,
|
||||
maxSessionTurns: settings.model?.maxSessionTurns,
|
||||
|
||||
experimentalZedIntegration: argv.experimentalAcp || false,
|
||||
features: settings.features,
|
||||
featureGates: argv.featureGates,
|
||||
listExtensions: argv.listExtensions || false,
|
||||
listSessions: argv.listSessions || false,
|
||||
deleteSession: argv.deleteSession,
|
||||
enabledExtensions: argv.extensions,
|
||||
extensionLoader: extensionManager,
|
||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
plan: settings.experimental?.plan,
|
||||
enableExtensionReloading: isFeatureEnabled(settings, 'extensionReloading'),
|
||||
enableAgents: isFeatureEnabled(settings, 'enableAgents'),
|
||||
plan: isFeatureEnabled(settings, 'plan'),
|
||||
tracker: settings.experimental?.taskTracker,
|
||||
directWebFetch: settings.experimental?.directWebFetch,
|
||||
planSettings: settings.general?.plan?.directory
|
||||
@@ -776,9 +786,14 @@ export async function loadCliConfig(
|
||||
enableEventDrivenScheduler: true,
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
experimentalJitContext: isFeatureEnabled(settings, 'jitContext'),
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
toolOutputMasking: settings.experimental?.toolOutputMasking,
|
||||
toolOutputMasking:
|
||||
(settings.features as Record<string, unknown> | undefined)?.[
|
||||
'toolOutputMasking'
|
||||
] !== undefined
|
||||
? { enabled: isFeatureEnabled(settings, 'toolOutputMasking') }
|
||||
: settings.experimental?.toolOutputMasking,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
ideMode,
|
||||
|
||||
@@ -9,7 +9,11 @@ import * as path from 'node:path';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import chalk from 'chalk';
|
||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
import { type MergedSettings, SettingScope } from './settings.js';
|
||||
import {
|
||||
type MergedSettings,
|
||||
SettingScope,
|
||||
isFeatureEnabled,
|
||||
} from './settings.js';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
|
||||
import {
|
||||
@@ -323,7 +327,10 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(destinationPath, { recursive: true });
|
||||
if (this.requestSetting && this.settings.experimental.extensionConfig) {
|
||||
if (
|
||||
this.requestSetting &&
|
||||
isFeatureEnabled(this.settings, 'extensionConfig')
|
||||
) {
|
||||
if (isUpdate) {
|
||||
await maybePromptForSettings(
|
||||
newExtensionConfig,
|
||||
@@ -341,7 +348,10 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
}
|
||||
|
||||
const missingSettings = this.settings.experimental.extensionConfig
|
||||
const missingSettings = isFeatureEnabled(
|
||||
this.settings,
|
||||
'extensionConfig',
|
||||
)
|
||||
? await getMissingSettings(
|
||||
newExtensionConfig,
|
||||
extensionId,
|
||||
@@ -677,7 +687,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
let userSettings: Record<string, string> = {};
|
||||
let workspaceSettings: Record<string, string> = {};
|
||||
|
||||
if (this.settings.experimental.extensionConfig) {
|
||||
if (isFeatureEnabled(this.settings, 'extensionConfig')) {
|
||||
userSettings = await getScopedEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
@@ -697,7 +707,10 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
config = resolveEnvVarsInObject(config, customEnv);
|
||||
|
||||
const resolvedSettings: ResolvedExtensionSetting[] = [];
|
||||
if (config.settings && this.settings.experimental.extensionConfig) {
|
||||
if (
|
||||
config.settings &&
|
||||
isFeatureEnabled(this.settings, 'extensionConfig')
|
||||
) {
|
||||
for (const setting of config.settings) {
|
||||
const value = customEnv[setting.envVar];
|
||||
let scope: 'user' | 'workspace' | undefined;
|
||||
|
||||
@@ -206,7 +206,13 @@ describe('extension tests', () => {
|
||||
});
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||
const settings = loadSettings(tempWorkspaceDir).merged;
|
||||
settings.experimental.extensionConfig = true;
|
||||
if (settings.features) {
|
||||
(settings.features as Record<string, unknown>)['extensionConfig'] = true;
|
||||
} else {
|
||||
(settings as unknown as Record<string, unknown>)['features'] = {
|
||||
extensionConfig: true,
|
||||
};
|
||||
}
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
|
||||
@@ -76,6 +76,8 @@ import {
|
||||
LoadedSettings,
|
||||
sanitizeEnvVar,
|
||||
createTestMergedSettings,
|
||||
isFeatureEnabled,
|
||||
type MergedSettings,
|
||||
} from './settings.js';
|
||||
import {
|
||||
FatalConfigError,
|
||||
@@ -3119,4 +3121,50 @@ describe('LoadedSettings Isolation and Serializability', () => {
|
||||
}).toThrow(/Maximum call stack size exceeded/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFeatureEnabled', () => {
|
||||
it('should return true if feature is enabled in "features"', () => {
|
||||
const settings = {
|
||||
features: { plan: true },
|
||||
} as unknown as Settings; // Casting for simplicity
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if feature is disabled in "features"', () => {
|
||||
const settings = {
|
||||
features: { plan: false },
|
||||
} as unknown as Settings;
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should fallback to "experimental" if feature is not in "features"', () => {
|
||||
const settings = {
|
||||
experimental: { plan: true },
|
||||
} as unknown as Settings;
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize "features" over "experimental"', () => {
|
||||
const settings = {
|
||||
features: { plan: false },
|
||||
experimental: { plan: true },
|
||||
} as unknown as Settings;
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if neither is set', () => {
|
||||
const settings = {} as unknown as Settings;
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
coreEvents,
|
||||
homedir,
|
||||
type AdminControlsSettings,
|
||||
DefaultFeatureGate,
|
||||
type FeatureGate,
|
||||
} from '@google/gemini-cli-core';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
|
||||
@@ -43,6 +45,57 @@ export {
|
||||
getSettingsSchema,
|
||||
};
|
||||
|
||||
const featureGateCache = new WeakMap<MergedSettings, FeatureGate>();
|
||||
|
||||
/**
|
||||
* Returns true if the feature is enabled in the given settings.
|
||||
* Checks "features" first, then falls back to "experimental".
|
||||
*/
|
||||
export function isFeatureEnabled(
|
||||
settings: MergedSettings,
|
||||
featureName: string,
|
||||
): boolean {
|
||||
let gate = featureGateCache.get(settings);
|
||||
if (!gate) {
|
||||
const mutableGate = DefaultFeatureGate.deepCopy();
|
||||
const overrides: Record<string, boolean> = {};
|
||||
|
||||
// 1. Experimental (Low Priority)
|
||||
const experimental = settings.experimental as Record<string, unknown>;
|
||||
if (experimental) {
|
||||
for (const [key, value] of Object.entries(experimental)) {
|
||||
if (typeof value === 'boolean') {
|
||||
overrides[key] = value;
|
||||
}
|
||||
}
|
||||
// Handle nested toolOutputMasking legacy structure
|
||||
const toolOutputMasking = experimental['toolOutputMasking'];
|
||||
if (
|
||||
toolOutputMasking &&
|
||||
typeof toolOutputMasking === 'object' &&
|
||||
'enabled' in toolOutputMasking &&
|
||||
typeof (toolOutputMasking as Record<string, unknown>)['enabled'] ===
|
||||
'boolean'
|
||||
) {
|
||||
overrides['toolOutputMasking'] = Boolean(
|
||||
(toolOutputMasking as Record<string, unknown>)['enabled'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Features (High Priority)
|
||||
if (settings.features) {
|
||||
Object.assign(overrides, settings.features);
|
||||
}
|
||||
|
||||
mutableGate.setFromMap(overrides);
|
||||
gate = mutableGate;
|
||||
featureGateCache.set(settings, gate);
|
||||
}
|
||||
|
||||
return gate.enabled(featureName);
|
||||
}
|
||||
|
||||
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
|
||||
import { customDeepMerge } from '../utils/deepMerge.js';
|
||||
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
|
||||
|
||||
@@ -1687,7 +1687,9 @@ const SETTINGS_SCHEMA = {
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Setting to enable experimental features',
|
||||
ignoreInDocs: true,
|
||||
description:
|
||||
'DEPRECATED: Use the "features" object instead. Setting to enable experimental features',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
toolOutputMasking: {
|
||||
@@ -1708,7 +1710,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enables tool output masking to save tokens.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
toolProtectionThreshold: {
|
||||
type: 'number',
|
||||
@@ -1825,7 +1827,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable planning features (Plan Mode and tools).',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
taskTracker: {
|
||||
type: 'boolean',
|
||||
@@ -2277,6 +2279,127 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
features: {
|
||||
type: 'object',
|
||||
label: 'Features',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Feature Lifecycle Management settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
allAlpha: {
|
||||
type: 'boolean',
|
||||
label: 'Enable all Alpha features',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable all Alpha features by default.',
|
||||
showInDialog: true,
|
||||
},
|
||||
allBeta: {
|
||||
type: 'boolean',
|
||||
label: 'Enable all Beta features',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable all Beta features by default.',
|
||||
showInDialog: true,
|
||||
},
|
||||
toolOutputMasking: {
|
||||
type: 'boolean',
|
||||
label: 'Tool Output Masking',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enables tool output masking to save tokens.',
|
||||
showInDialog: true,
|
||||
},
|
||||
enableAgents: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Agents',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable local and remote subagents.',
|
||||
showInDialog: true,
|
||||
},
|
||||
extensionManagement: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Management',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable extension management features.',
|
||||
showInDialog: true,
|
||||
},
|
||||
extensionConfig: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Configuration',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable requesting and fetching of extension settings.',
|
||||
showInDialog: true,
|
||||
},
|
||||
extensionRegistry: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Registry Explore UI',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable extension registry explore UI.',
|
||||
showInDialog: true,
|
||||
},
|
||||
extensionReloading: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Reloading',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enables extension loading/unloading within the CLI session.',
|
||||
showInDialog: true,
|
||||
},
|
||||
jitContext: {
|
||||
type: 'boolean',
|
||||
label: 'JIT Context Loading',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable Just-In-Time (JIT) context loading.',
|
||||
showInDialog: true,
|
||||
},
|
||||
useOSC52Paste: {
|
||||
type: 'boolean',
|
||||
label: 'Use OSC 52 Paste',
|
||||
category: 'Features',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Use OSC 52 sequence for pasting.',
|
||||
showInDialog: true,
|
||||
},
|
||||
plan: {
|
||||
type: 'boolean',
|
||||
label: 'Plan Mode',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable planning features (Plan Mode and tools).',
|
||||
showInDialog: true,
|
||||
},
|
||||
zedIntegration: {
|
||||
type: 'boolean',
|
||||
label: 'Zed Integration',
|
||||
category: 'Features',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable Zed integration.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies SettingsSchema;
|
||||
|
||||
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
|
||||
|
||||
@@ -415,7 +415,7 @@ describe('SettingsDialog', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Should wrap to last setting (without relying on exact bullet character)
|
||||
expect(lastFrame()).toContain('Hook Notifications');
|
||||
expect(lastFrame()).toContain('Zed Integration');
|
||||
});
|
||||
|
||||
unmount();
|
||||
@@ -753,6 +753,33 @@ describe('SettingsDialog', () => {
|
||||
});
|
||||
|
||||
describe('Specific Settings Behavior', () => {
|
||||
it('should render feature stage badges', async () => {
|
||||
const featureSettings = createMockSettings({
|
||||
'features.allAlpha': false,
|
||||
'features.allBeta': true,
|
||||
});
|
||||
|
||||
const onSelect = vi.fn();
|
||||
const { lastFrame, stdin } = render(
|
||||
<KeypressProvider>
|
||||
<SettingsDialog
|
||||
settings={featureSettings}
|
||||
onSelect={onSelect}
|
||||
availableTerminalHeight={100}
|
||||
/>
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
stdin.write('Plan');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Plan Mode');
|
||||
expect(lastFrame()).toContain('[ALPHA]');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show correct display values for settings with different states', async () => {
|
||||
const settings = createMockSettings({
|
||||
user: {
|
||||
|
||||
@@ -33,7 +33,12 @@ import {
|
||||
type SettingsValue,
|
||||
TOGGLE_TYPES,
|
||||
} from '../../config/settingsSchema.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import {
|
||||
coreEvents,
|
||||
debugLogger,
|
||||
FeatureDefinitions,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
import { useSearchBuffer } from '../hooks/useSearchBuffer.js';
|
||||
import {
|
||||
@@ -244,6 +249,14 @@ export function SettingsDialog({
|
||||
// The inline editor needs a string but non primitive settings like Arrays and Objects exist
|
||||
const editValue = getEditValue(type, rawValue);
|
||||
|
||||
let stage: string | undefined;
|
||||
const definitionKey = key.replace(/^features\./, '');
|
||||
if (key.startsWith('features.') && FeatureDefinitions[definitionKey]) {
|
||||
const specs = FeatureDefinitions[definitionKey];
|
||||
const latest = specs[specs.length - 1];
|
||||
stage = latest.preRelease;
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
label: definition?.label || key,
|
||||
@@ -252,8 +265,10 @@ export function SettingsDialog({
|
||||
displayValue,
|
||||
isGreyedOut,
|
||||
scopeMessage,
|
||||
rawValue,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
rawValue: rawValue as string | number | boolean | undefined,
|
||||
editValue,
|
||||
stage,
|
||||
};
|
||||
});
|
||||
}, [settingKeys, selectedScope, settings]);
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { formatCommand } from '../../utils/keybindingUtils.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
|
||||
/**
|
||||
* Represents a single item in the settings dialog.
|
||||
@@ -49,6 +50,8 @@ export interface SettingsDialogItem {
|
||||
rawValue?: SettingsValue;
|
||||
/** Optional pre-formatted edit buffer value for complex types */
|
||||
editValue?: string;
|
||||
/** Feature stage (e.g. ALPHA, BETA) */
|
||||
stage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -553,6 +556,20 @@ export function BaseSettingsDialog({
|
||||
color={isActive ? theme.ui.focus : theme.text.primary}
|
||||
>
|
||||
{item.label}
|
||||
{item.stage && item.stage !== FeatureStage.GA && (
|
||||
<Text
|
||||
color={
|
||||
item.stage === FeatureStage.Deprecated
|
||||
? theme.status.error
|
||||
: item.stage === FeatureStage.Beta
|
||||
? theme.text.accent
|
||||
: theme.status.warning
|
||||
}
|
||||
>
|
||||
{' '}
|
||||
[{item.stage}]{' '}
|
||||
</Text>
|
||||
)}
|
||||
{item.scopeMessage && (
|
||||
<Text color={theme.text.secondary}>
|
||||
{' '}
|
||||
|
||||
Reference in New Issue
Block a user