mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
feat(core): implement feature lifecycle management (Alpha, Beta, GA)
This commit is contained in:
@@ -4,6 +4,9 @@
|
|||||||
"extensionReloading": true,
|
"extensionReloading": true,
|
||||||
"modelSteering": true
|
"modelSteering": true
|
||||||
},
|
},
|
||||||
|
"features": {
|
||||||
|
"allAlpha": true
|
||||||
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"devtools": true
|
"devtools": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Feature Lifecycle
|
||||||
|
|
||||||
|
Gemini CLI uses a **Feature Lifecycle** management system to handle the
|
||||||
|
introduction, maturation, and deprecation of new and optional features.
|
||||||
|
|
||||||
|
- [Feature Lifecycle](#feature-lifecycle)
|
||||||
|
- [Feature Stages](#feature-stages)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Precedence and Reconciliation](#precedence-and-reconciliation)
|
||||||
|
- [Available Features](#available-features)
|
||||||
|
- [Managing Feature Lifecycle](#managing-feature-lifecycle)
|
||||||
|
- [Adding a Feature](#adding-a-feature)
|
||||||
|
- [Promoting a Feature](#promoting-a-feature)
|
||||||
|
- [Deprecating a Feature](#deprecating-a-feature)
|
||||||
|
- [Removing a Feature](#removing-a-feature)
|
||||||
|
- [Relevant Documentation](#relevant-documentation)
|
||||||
|
|
||||||
|
## Feature Stages
|
||||||
|
|
||||||
|
Features progress through the following stages:
|
||||||
|
|
||||||
|
| Stage | Default | UI Badge | Description |
|
||||||
|
| -------------- | -------- | -------------- | ------------------------------------------------------------------------------------------- |
|
||||||
|
| **ALPHA** | Disabled | `[ALPHA]` | Early-access features. May be unstable, change significantly, or be removed without notice. |
|
||||||
|
| **BETA** | Enabled | `[BETA]` | Features that are well-tested and considered stable. Can be disabled if issues arise. |
|
||||||
|
| **GA** | Enabled | - | Stable features that are part of the core product. Cannot be disabled. |
|
||||||
|
| **DEPRECATED** | Disabled | `[DEPRECATED]` | Features scheduled for removal. Using them triggers a warning. |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The feature lifecycle can be configured in several ways:
|
||||||
|
|
||||||
|
1. **`settings.json`**: Use the `features` object to toggle specific features
|
||||||
|
or entire stages.
|
||||||
|
- `features.allAlpha`: Enable/disable all Alpha features.
|
||||||
|
- `features.allBeta`: Enable/disable all Beta features.
|
||||||
|
- `features.<featureName>`: Toggle an individual feature.
|
||||||
|
2. **CLI Flag**: Use `--feature-gates="feat1=true,feat2=false"` for runtime
|
||||||
|
overrides.
|
||||||
|
3. **Environment Variable**: Set
|
||||||
|
`GEMINI_FEATURE_GATES="feat1=true,feat2=false"`.
|
||||||
|
|
||||||
|
The stability of each feature is visually indicated in the
|
||||||
|
[`/settings` UI](/docs/cli/settings.md) with colored badges. **GA** features are
|
||||||
|
considerd stable and look identical to standard settings.
|
||||||
|
|
||||||
|
### Precedence and Reconciliation
|
||||||
|
|
||||||
|
When determining if a feature is enabled, the system follows this order of
|
||||||
|
precedence (highest priority first):
|
||||||
|
|
||||||
|
1. **Global Lock**: Features in the **GA** stage are locked to `true` and cannot
|
||||||
|
be disabled.
|
||||||
|
2. **CLI Flags & Environment Variables**: Runtime overrides (`--feature-gates`
|
||||||
|
or `GEMINI_FEATURE_GATES`) override persistent settings.
|
||||||
|
3. **Individual Toggle**: Specific feature toggles in `settings.json` (e.g.,
|
||||||
|
`"features": { "plan": true }`).
|
||||||
|
4. **Meta Toggles**: Stage-wide toggles in `settings.json` (`allAlpha` or
|
||||||
|
`allBeta`). For example, if `allAlpha` is `true`, all Alpha features are
|
||||||
|
enabled unless specifically disabled by an individual toggle.
|
||||||
|
5. **Stage Default**: The inherent default for the feature's current stage
|
||||||
|
(Alpha: Disabled, Beta/GA: Enabled).
|
||||||
|
|
||||||
|
For more details on persistent configuration, see the [Configuration guide].
|
||||||
|
|
||||||
|
## Available Features
|
||||||
|
|
||||||
|
<!-- FEATURES-AUTOGEN:START -->
|
||||||
|
|
||||||
|
| Feature | Stage | Default | Since | Description |
|
||||||
|
| --------------------- | ----- | -------- | ------ | ----------------------------------------------------------- |
|
||||||
|
| `enableAgents` | ALPHA | Disabled | 0.30.0 | Enable local and remote subagents. |
|
||||||
|
| `extensionConfig` | BETA | Enabled | 0.30.0 | Enable requesting and fetching of extension settings. |
|
||||||
|
| `extensionManagement` | BETA | Enabled | 0.30.0 | Enable extension management features. |
|
||||||
|
| `extensionRegistry` | ALPHA | Disabled | 0.30.0 | Enable extension registry explore UI. |
|
||||||
|
| `extensionReloading` | ALPHA | Disabled | 0.30.0 | Enables extension loading/unloading within the CLI session. |
|
||||||
|
| `jitContext` | ALPHA | Disabled | 0.30.0 | Enable Just-In-Time (JIT) context loading. |
|
||||||
|
| `plan` | ALPHA | Disabled | 0.30.0 | Enable planning features (Plan Mode and tools). |
|
||||||
|
| `toolOutputMasking` | BETA | Enabled | 0.30.0 | Enables tool output masking to save tokens. |
|
||||||
|
| `useOSC52Paste` | ALPHA | Disabled | 0.30.0 | Use OSC 52 sequence for pasting. |
|
||||||
|
| `zedIntegration` | ALPHA | Disabled | 0.30.0 | Enable Zed integration. |
|
||||||
|
|
||||||
|
<!-- FEATURES-AUTOGEN:END -->
|
||||||
|
|
||||||
|
## Managing Feature Lifecycle
|
||||||
|
|
||||||
|
Maintaining a feature involves promoting it through stages or eventually
|
||||||
|
deprecating and removing it.
|
||||||
|
|
||||||
|
### Adding a Feature
|
||||||
|
|
||||||
|
To add a new feature under lifecycle management:
|
||||||
|
|
||||||
|
1. **Define the Feature**: Add a new entry to [`FeatureDefinitions`] in
|
||||||
|
[`features.ts`].
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const FeatureDefinitions: Record<string, FeatureSpec[]> = {
|
||||||
|
// ... existing features
|
||||||
|
myNewFeature: [
|
||||||
|
{
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
since: '0.31.0',
|
||||||
|
description: 'Description of my new feature.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
_Note: The `default` field is optional. If omitted, it defaults to `false`
|
||||||
|
for Alpha/Deprecated and `true` for Beta/GA._
|
||||||
|
|
||||||
|
2. **Expose in Settings**: Add the feature to the `features` object in
|
||||||
|
[`settingsSchema.ts`]. This ensures it appears in the `/settings` UI and is
|
||||||
|
validated.
|
||||||
|
```typescript
|
||||||
|
features: {
|
||||||
|
// ...
|
||||||
|
properties: {
|
||||||
|
// ...
|
||||||
|
myNewFeature: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'My New Feature',
|
||||||
|
category: 'Features',
|
||||||
|
requiresRestart: true, // or false
|
||||||
|
description: 'Description of my new feature.',
|
||||||
|
showInDialog: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
3. **Use the Feature**: In your code, check if the feature is enabled using the
|
||||||
|
`Config` object.
|
||||||
|
```typescript
|
||||||
|
if (this.config.isFeatureEnabled('myNewFeature')) {
|
||||||
|
// Feature logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Promoting a Feature
|
||||||
|
|
||||||
|
When a feature is ready for the next stage:
|
||||||
|
|
||||||
|
1. **Update [`features.ts`]**: Add a new `FeatureSpec` to the feature's array.
|
||||||
|
- **To BETA**: Set `preRelease: FeatureStage.Beta` (Defaults to `true`).
|
||||||
|
- **To GA**: Set `preRelease: FeatureStage.GA` (Defaults to `true` and
|
||||||
|
locked).
|
||||||
|
- Update the `since` version.
|
||||||
|
2. **Update [`settingsSchema.ts`]**: Update the `label` and `description` if
|
||||||
|
necessary.
|
||||||
|
3. **GA Cleanup**: Once a feature is GA and no longer optional, remove the
|
||||||
|
feature gate check from the code and make it a core part of the logic.
|
||||||
|
|
||||||
|
### Deprecating a Feature
|
||||||
|
|
||||||
|
This stage is for **Beta** and **GA** features scheduled for removal.
|
||||||
|
|
||||||
|
1. **Update [`features.ts`]**: Add a new `FeatureSpec` with
|
||||||
|
`preRelease: FeatureStage.Deprecated`.
|
||||||
|
- Optionally set `default: true` if it should remain enabled during
|
||||||
|
deprecation (it defaults to `false`).
|
||||||
|
- Optionally set an `until` version to indicate when it will be removed.
|
||||||
|
2. **Update [`settingsSchema.ts`]**: Update the description to notify users of
|
||||||
|
the deprecation and suggest alternatives.
|
||||||
|
|
||||||
|
### Removing a Feature
|
||||||
|
|
||||||
|
> **Alpha** features can be removed without formal deprecation. **Beta** and
|
||||||
|
> **GA** features should typically go through a
|
||||||
|
> [deprecation period](#deprecating-a-feature) first.
|
||||||
|
|
||||||
|
To completely remove a feature:
|
||||||
|
|
||||||
|
1. **Cleanup Code**: Remove all logic and tests associated with the feature.
|
||||||
|
2. **Update [`features.ts`]**: Remove the feature from [`FeatureDefinitions`].
|
||||||
|
3. **Update [`settingsSchema.ts`]**: Remove the feature from the `features`
|
||||||
|
object.
|
||||||
|
4. **Legacy Settings**: If the feature had a legacy `experimental` flag, ensure
|
||||||
|
its migration logic is cleaned up in [`config.ts`].
|
||||||
|
|
||||||
|
## Relevant Documentation
|
||||||
|
|
||||||
|
- [Settings Reference]
|
||||||
|
- [Configuration Layers]
|
||||||
|
|
||||||
|
[Configuration guide]: /docs/get-started/configuration.md
|
||||||
|
[Settings Reference]: /docs/cli/settings.md#features
|
||||||
|
[Configuration Layers]: /docs/get-started/configuration.md#configuration-layers
|
||||||
|
[`FeatureDefinitions`]: /packages/core/src/config/features.ts
|
||||||
|
[`features.ts`]: /packages/core/src/config/features.ts
|
||||||
|
[`settingsSchema.ts`]: /packages/cli/src/config/settingsSchema.ts
|
||||||
|
[`config.ts`]: /packages/core/src/config/config.ts
|
||||||
@@ -162,4 +162,21 @@ they appear in the UI.
|
|||||||
| Enable Hooks | `hooksConfig.enabled` | Canonical toggle for the hooks system. When disabled, no hooks will be executed. | `true` |
|
| Enable Hooks | `hooksConfig.enabled` | Canonical toggle for the hooks system. When disabled, no hooks will be executed. | `true` |
|
||||||
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
|
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
| UI Label | Setting | Description | Default | Stage |
|
||||||
|
| ----------------------------- | ------------------------------ | ----------------------------------------------------------- | ------- | ------- |
|
||||||
|
| Enable all Alpha features | `features.allAlpha` | Enable all Alpha features by default. | `false` | - |
|
||||||
|
| Enable all Beta features | `features.allBeta` | Enable all Beta features by default. | `true` | - |
|
||||||
|
| Tool Output Masking | `features.toolOutputMasking` | Enables tool output masking to save tokens. | `true` | `BETA` |
|
||||||
|
| Enable Agents | `features.enableAgents` | Enable local and remote subagents. | `false` | `ALPHA` |
|
||||||
|
| Extension Management | `features.extensionManagement` | Enable extension management features. | `true` | `BETA` |
|
||||||
|
| Extension Configuration | `features.extensionConfig` | Enable requesting and fetching of extension settings. | `true` | `BETA` |
|
||||||
|
| Extension Registry Explore UI | `features.extensionRegistry` | Enable extension registry explore UI. | `false` | `ALPHA` |
|
||||||
|
| Extension Reloading | `features.extensionReloading` | Enables extension loading/unloading within the CLI session. | `false` | `ALPHA` |
|
||||||
|
| JIT Context Loading | `features.jitContext` | Enable Just-In-Time (JIT) context loading. | `false` | `ALPHA` |
|
||||||
|
| Use OSC 52 Paste | `features.useOSC52Paste` | Use OSC 52 sequence for pasting. | `false` | `ALPHA` |
|
||||||
|
| Plan Mode | `features.plan` | Enable planning features (Plan Mode and tools). | `false` | `ALPHA` |
|
||||||
|
| Zed Integration | `features.zedIntegration` | Enable Zed integration. | `false` | `ALPHA` |
|
||||||
|
|
||||||
<!-- SETTINGS-AUTOGEN:END -->
|
<!-- SETTINGS-AUTOGEN:END -->
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ contain other project-specific files related to Gemini CLI's operation, such as:
|
|||||||
Settings are organized into categories. All settings should be placed within
|
Settings are organized into categories. All settings should be placed within
|
||||||
their corresponding top-level category object in your `settings.json` file.
|
their corresponding top-level category object in your `settings.json` file.
|
||||||
|
|
||||||
|
#### Feature Lifecycle
|
||||||
|
|
||||||
|
Gemini CLI uses a feature lifecycle management system to manage experimental and
|
||||||
|
optional features. Features are categorized by their stability stage (`ALPHA`,
|
||||||
|
`BETA`, `GA`, `DEPRECATED`).
|
||||||
|
|
||||||
|
For a detailed explanation of feature stages and a list of all available
|
||||||
|
features, see the
|
||||||
|
[Feature Lifecycle documentation](/docs/cli/feature-lifecycle.md).
|
||||||
|
|
||||||
<!-- SETTINGS-AUTOGEN:START -->
|
<!-- SETTINGS-AUTOGEN:START -->
|
||||||
|
|
||||||
#### `policyPaths`
|
#### `policyPaths`
|
||||||
@@ -1056,7 +1066,6 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
`gemma3-1b-gpu-custom`.
|
`gemma3-1b-gpu-custom`.
|
||||||
- **Default:** `"gemma3-1b-gpu-custom"`
|
- **Default:** `"gemma3-1b-gpu-custom"`
|
||||||
- **Requires restart:** Yes
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
#### `skills`
|
#### `skills`
|
||||||
|
|
||||||
- **`skills.enabled`** (boolean):
|
- **`skills.enabled`** (boolean):
|
||||||
@@ -1165,6 +1174,77 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
- **`admin.skills.enabled`** (boolean):
|
- **`admin.skills.enabled`** (boolean):
|
||||||
- **Description:** If false, disallows agent skills from being used.
|
- **Description:** If false, disallows agent skills from being used.
|
||||||
- **Default:** `true`
|
- **Default:** `true`
|
||||||
|
|
||||||
|
#### `features`
|
||||||
|
|
||||||
|
- **`features.allAlpha`** (boolean):
|
||||||
|
- **Description:** Enable all Alpha features by default.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.allBeta`** (boolean):
|
||||||
|
- **Description:** Enable all Beta features by default.
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.toolOutputMasking`** (boolean):
|
||||||
|
- **Description:** Enables tool output masking to save tokens.
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Stage:** BETA
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.enableAgents`** (boolean):
|
||||||
|
- **Description:** Enable local and remote subagents.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Stage:** ALPHA
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.extensionManagement`** (boolean):
|
||||||
|
- **Description:** Enable extension management features.
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Stage:** BETA
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.extensionConfig`** (boolean):
|
||||||
|
- **Description:** Enable requesting and fetching of extension settings.
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Stage:** BETA
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.extensionRegistry`** (boolean):
|
||||||
|
- **Description:** Enable extension registry explore UI.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Stage:** ALPHA
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.extensionReloading`** (boolean):
|
||||||
|
- **Description:** Enables extension loading/unloading within the CLI session.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Stage:** ALPHA
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.jitContext`** (boolean):
|
||||||
|
- **Description:** Enable Just-In-Time (JIT) context loading.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Stage:** ALPHA
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.useOSC52Paste`** (boolean):
|
||||||
|
- **Description:** Use OSC 52 sequence for pasting.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Stage:** ALPHA
|
||||||
|
|
||||||
|
- **`features.plan`** (boolean):
|
||||||
|
- **Description:** Enable planning features (Plan Mode and tools).
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Stage:** ALPHA
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`features.zedIntegration`** (boolean):
|
||||||
|
- **Description:** Enable Zed integration.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Stage:** ALPHA
|
||||||
|
- **Requires restart:** Yes
|
||||||
<!-- SETTINGS-AUTOGEN:END -->
|
<!-- SETTINGS-AUTOGEN:END -->
|
||||||
|
|
||||||
#### `mcpServers`
|
#### `mcpServers`
|
||||||
@@ -1350,6 +1430,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
|||||||
is useful when running Gemini CLI in a standalone terminal while still
|
is useful when running Gemini CLI in a standalone terminal while still
|
||||||
wanting to associate it with a specific IDE instance.
|
wanting to associate it with a specific IDE instance.
|
||||||
- Overrides the automatic IDE detection logic.
|
- Overrides the automatic IDE detection logic.
|
||||||
|
- **`GEMINI_FEATURE_GATES`**:
|
||||||
|
- Specifies a comma-separated list of feature key-value pairs to override the
|
||||||
|
default feature states.
|
||||||
|
- Example: `export GEMINI_FEATURE_GATES="plan=true,enableAgents=false"`
|
||||||
- **`GEMINI_CLI_HOME`**:
|
- **`GEMINI_CLI_HOME`**:
|
||||||
- Specifies the root directory for Gemini CLI's user-level configuration and
|
- Specifies the root directory for Gemini CLI's user-level configuration and
|
||||||
storage.
|
storage.
|
||||||
@@ -1551,13 +1635,17 @@ for that specific session.
|
|||||||
- `auto_edit`: Automatically approve edit tools (replace, write_file) while
|
- `auto_edit`: Automatically approve edit tools (replace, write_file) while
|
||||||
prompting for others
|
prompting for others
|
||||||
- `yolo`: Automatically approve all tool calls (equivalent to `--yolo`)
|
- `yolo`: Automatically approve all tool calls (equivalent to `--yolo`)
|
||||||
- `plan`: Read-only mode for tool calls (requires experimental planning to
|
- `plan`: Read-only mode for tool calls (requires planning feature to be
|
||||||
be enabled).
|
enabled).
|
||||||
> **Note:** This mode is currently under development and not yet fully
|
> **Note:** This mode is currently under development and not yet fully
|
||||||
> functional.
|
> functional.
|
||||||
- Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of
|
- Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of
|
||||||
`--yolo` for the new unified approach.
|
`--yolo` for the new unified approach.
|
||||||
- Example: `gemini --approval-mode auto_edit`
|
- Example: `gemini --approval-mode auto_edit`
|
||||||
|
- **`--feature-gates <key1=val1,key2=val2,...>`**:
|
||||||
|
- A comma-separated list of feature key-value pairs to override the default
|
||||||
|
feature states for this session.
|
||||||
|
- Example: `gemini --feature-gates "plan=true,enableAgents=false"`
|
||||||
- **`--allowed-tools <tool1,tool2,...>`**:
|
- **`--allowed-tools <tool1,tool2,...>`**:
|
||||||
- A comma-separated list of tool names that will bypass the confirmation
|
- A comma-separated list of tool names that will bypass the confirmation
|
||||||
dialog.
|
dialog.
|
||||||
|
|||||||
@@ -161,6 +161,14 @@
|
|||||||
],
|
],
|
||||||
"*.{json,md}": [
|
"*.{json,md}": [
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"packages/cli/src/config/settingsSchema.ts": [
|
||||||
|
"npm run docs:settings",
|
||||||
|
"git add schemas/settings.schema.json docs/get-started/configuration.md docs/cli/settings.md docs/cli/feature-lifecycle.md"
|
||||||
|
],
|
||||||
|
"packages/cli/src/config/keyBindings.ts": [
|
||||||
|
"npm run docs:keybindings",
|
||||||
|
"git add docs/cli/keyboard-shortcuts.md"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
configureSpecificSetting,
|
configureSpecificSetting,
|
||||||
getExtensionManager,
|
getExtensionManager,
|
||||||
} from './utils.js';
|
} 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 { coreEvents, debugLogger } from '@google/gemini-cli-core';
|
||||||
import { exitCli } from '../utils.js';
|
import { exitCli } from '../utils.js';
|
||||||
|
|
||||||
@@ -45,10 +45,10 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
|
|||||||
const { name, setting, scope } = args;
|
const { name, setting, scope } = args;
|
||||||
const settings = loadSettings(process.cwd()).merged;
|
const settings = loadSettings(process.cwd()).merged;
|
||||||
|
|
||||||
if (!(settings.experimental?.extensionConfig ?? true)) {
|
if (!isFeatureEnabled(settings, 'extensionConfig')) {
|
||||||
coreEvents.emitFeedback(
|
coreEvents.emitFeedback(
|
||||||
'error',
|
'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();
|
await exitCli();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2647,6 +2647,62 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
expect(plansDir).toContain('.custom-plans');
|
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 ---
|
// --- Untrusted Folder Scenarios ---
|
||||||
describe('when folder is NOT trusted', () => {
|
describe('when folder is NOT trusted', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
type MergedSettings,
|
type MergedSettings,
|
||||||
saveModelChange,
|
saveModelChange,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
|
isFeatureEnabled,
|
||||||
} from './settings.js';
|
} from './settings.js';
|
||||||
|
|
||||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||||
@@ -78,6 +79,7 @@ export interface CliArgs {
|
|||||||
allowedTools: string[] | undefined;
|
allowedTools: string[] | undefined;
|
||||||
acp?: boolean;
|
acp?: boolean;
|
||||||
experimentalAcp?: boolean;
|
experimentalAcp?: boolean;
|
||||||
|
featureGates?: string | undefined;
|
||||||
extensions: string[] | undefined;
|
extensions: string[] | undefined;
|
||||||
listExtensions: boolean | undefined;
|
listExtensions: boolean | undefined;
|
||||||
resume: string | typeof RESUME_LATEST | undefined;
|
resume: string | typeof RESUME_LATEST | undefined;
|
||||||
@@ -182,6 +184,12 @@ export async function parseArguments(
|
|||||||
description:
|
description:
|
||||||
'Starts the agent in ACP mode (deprecated, use --acp instead)',
|
'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', {
|
.option('allowed-mcp-server-names', {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
string: true,
|
string: true,
|
||||||
@@ -328,7 +336,7 @@ export async function parseArguments(
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (settings.experimental?.extensionManagement) {
|
if (isFeatureEnabled(settings, 'extensionManagement')) {
|
||||||
yargsInstance.command(extensionsCommand);
|
yargsInstance.command(extensionsCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +494,7 @@ export async function loadCliConfig(
|
|||||||
.getExtensions()
|
.getExtensions()
|
||||||
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
|
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
|
||||||
|
|
||||||
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
const experimentalJitContext = isFeatureEnabled(settings, 'jitContext');
|
||||||
|
|
||||||
let memoryContent: string | HierarchicalMemory = '';
|
let memoryContent: string | HierarchicalMemory = '';
|
||||||
let fileCount = 0;
|
let fileCount = 0;
|
||||||
@@ -532,7 +540,7 @@ export async function loadCliConfig(
|
|||||||
approvalMode = ApprovalMode.AUTO_EDIT;
|
approvalMode = ApprovalMode.AUTO_EDIT;
|
||||||
break;
|
break;
|
||||||
case 'plan':
|
case 'plan':
|
||||||
if (!(settings.experimental?.plan ?? false)) {
|
if (!isFeatureEnabled(settings, 'plan')) {
|
||||||
debugLogger.warn(
|
debugLogger.warn(
|
||||||
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
|
'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,
|
bugCommand: settings.advanced?.bugCommand,
|
||||||
model: resolvedModel,
|
model: resolvedModel,
|
||||||
maxSessionTurns: settings.model?.maxSessionTurns,
|
maxSessionTurns: settings.model?.maxSessionTurns,
|
||||||
|
experimentalZedIntegration: argv.experimentalAcp || false,
|
||||||
|
features: settings.features,
|
||||||
|
featureGates: argv.featureGates,
|
||||||
listExtensions: argv.listExtensions || false,
|
listExtensions: argv.listExtensions || false,
|
||||||
listSessions: argv.listSessions || false,
|
listSessions: argv.listSessions || false,
|
||||||
deleteSession: argv.deleteSession,
|
deleteSession: argv.deleteSession,
|
||||||
enabledExtensions: argv.extensions,
|
enabledExtensions: argv.extensions,
|
||||||
extensionLoader: extensionManager,
|
extensionLoader: extensionManager,
|
||||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
enableExtensionReloading: isFeatureEnabled(settings, 'extensionReloading'),
|
||||||
enableAgents: settings.experimental?.enableAgents,
|
enableAgents: isFeatureEnabled(settings, 'enableAgents'),
|
||||||
plan: settings.experimental?.plan,
|
plan: isFeatureEnabled(settings, 'plan'),
|
||||||
tracker: settings.experimental?.taskTracker,
|
tracker: settings.experimental?.taskTracker,
|
||||||
directWebFetch: settings.experimental?.directWebFetch,
|
directWebFetch: settings.experimental?.directWebFetch,
|
||||||
planSettings: settings.general?.plan?.directory
|
planSettings: settings.general?.plan?.directory
|
||||||
@@ -776,9 +786,14 @@ export async function loadCliConfig(
|
|||||||
enableEventDrivenScheduler: true,
|
enableEventDrivenScheduler: true,
|
||||||
skillsSupport: settings.skills?.enabled ?? true,
|
skillsSupport: settings.skills?.enabled ?? true,
|
||||||
disabledSkills: settings.skills?.disabled,
|
disabledSkills: settings.skills?.disabled,
|
||||||
experimentalJitContext: settings.experimental?.jitContext,
|
experimentalJitContext: isFeatureEnabled(settings, 'jitContext'),
|
||||||
modelSteering: settings.experimental?.modelSteering,
|
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'],
|
noBrowser: !!process.env['NO_BROWSER'],
|
||||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||||
ideMode,
|
ideMode,
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import * as path from 'node:path';
|
|||||||
import { stat } from 'node:fs/promises';
|
import { stat } from 'node:fs/promises';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
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 { createHash, randomUUID } from 'node:crypto';
|
||||||
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
|
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
|
||||||
import {
|
import {
|
||||||
@@ -323,7 +327,10 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.mkdir(destinationPath, { recursive: true });
|
await fs.promises.mkdir(destinationPath, { recursive: true });
|
||||||
if (this.requestSetting && this.settings.experimental.extensionConfig) {
|
if (
|
||||||
|
this.requestSetting &&
|
||||||
|
isFeatureEnabled(this.settings, 'extensionConfig')
|
||||||
|
) {
|
||||||
if (isUpdate) {
|
if (isUpdate) {
|
||||||
await maybePromptForSettings(
|
await maybePromptForSettings(
|
||||||
newExtensionConfig,
|
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(
|
? await getMissingSettings(
|
||||||
newExtensionConfig,
|
newExtensionConfig,
|
||||||
extensionId,
|
extensionId,
|
||||||
@@ -677,7 +687,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
let userSettings: Record<string, string> = {};
|
let userSettings: Record<string, string> = {};
|
||||||
let workspaceSettings: Record<string, string> = {};
|
let workspaceSettings: Record<string, string> = {};
|
||||||
|
|
||||||
if (this.settings.experimental.extensionConfig) {
|
if (isFeatureEnabled(this.settings, 'extensionConfig')) {
|
||||||
userSettings = await getScopedEnvContents(
|
userSettings = await getScopedEnvContents(
|
||||||
config,
|
config,
|
||||||
extensionId,
|
extensionId,
|
||||||
@@ -697,7 +707,10 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
config = resolveEnvVarsInObject(config, customEnv);
|
config = resolveEnvVarsInObject(config, customEnv);
|
||||||
|
|
||||||
const resolvedSettings: ResolvedExtensionSetting[] = [];
|
const resolvedSettings: ResolvedExtensionSetting[] = [];
|
||||||
if (config.settings && this.settings.experimental.extensionConfig) {
|
if (
|
||||||
|
config.settings &&
|
||||||
|
isFeatureEnabled(this.settings, 'extensionConfig')
|
||||||
|
) {
|
||||||
for (const setting of config.settings) {
|
for (const setting of config.settings) {
|
||||||
const value = customEnv[setting.envVar];
|
const value = customEnv[setting.envVar];
|
||||||
let scope: 'user' | 'workspace' | undefined;
|
let scope: 'user' | 'workspace' | undefined;
|
||||||
|
|||||||
@@ -206,7 +206,13 @@ describe('extension tests', () => {
|
|||||||
});
|
});
|
||||||
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||||
const settings = loadSettings(tempWorkspaceDir).merged;
|
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({
|
extensionManager = new ExtensionManager({
|
||||||
workspaceDir: tempWorkspaceDir,
|
workspaceDir: tempWorkspaceDir,
|
||||||
requestConsent: mockRequestConsent,
|
requestConsent: mockRequestConsent,
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ import {
|
|||||||
LoadedSettings,
|
LoadedSettings,
|
||||||
sanitizeEnvVar,
|
sanitizeEnvVar,
|
||||||
createTestMergedSettings,
|
createTestMergedSettings,
|
||||||
|
isFeatureEnabled,
|
||||||
|
type MergedSettings,
|
||||||
} from './settings.js';
|
} from './settings.js';
|
||||||
import {
|
import {
|
||||||
FatalConfigError,
|
FatalConfigError,
|
||||||
@@ -3119,4 +3121,50 @@ describe('LoadedSettings Isolation and Serializability', () => {
|
|||||||
}).toThrow(/Maximum call stack size exceeded/);
|
}).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,
|
coreEvents,
|
||||||
homedir,
|
homedir,
|
||||||
type AdminControlsSettings,
|
type AdminControlsSettings,
|
||||||
|
DefaultFeatureGate,
|
||||||
|
type FeatureGate,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import stripJsonComments from 'strip-json-comments';
|
import stripJsonComments from 'strip-json-comments';
|
||||||
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
|
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
|
||||||
@@ -43,6 +45,57 @@ export {
|
|||||||
getSettingsSchema,
|
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 { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
|
||||||
import { customDeepMerge } from '../utils/deepMerge.js';
|
import { customDeepMerge } from '../utils/deepMerge.js';
|
||||||
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
|
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
|
||||||
|
|||||||
@@ -1687,7 +1687,9 @@ const SETTINGS_SCHEMA = {
|
|||||||
category: 'Experimental',
|
category: 'Experimental',
|
||||||
requiresRestart: true,
|
requiresRestart: true,
|
||||||
default: {},
|
default: {},
|
||||||
description: 'Setting to enable experimental features',
|
ignoreInDocs: true,
|
||||||
|
description:
|
||||||
|
'DEPRECATED: Use the "features" object instead. Setting to enable experimental features',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
properties: {
|
properties: {
|
||||||
toolOutputMasking: {
|
toolOutputMasking: {
|
||||||
@@ -1708,7 +1710,7 @@ const SETTINGS_SCHEMA = {
|
|||||||
requiresRestart: true,
|
requiresRestart: true,
|
||||||
default: true,
|
default: true,
|
||||||
description: 'Enables tool output masking to save tokens.',
|
description: 'Enables tool output masking to save tokens.',
|
||||||
showInDialog: true,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
toolProtectionThreshold: {
|
toolProtectionThreshold: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -1825,7 +1827,7 @@ const SETTINGS_SCHEMA = {
|
|||||||
requiresRestart: true,
|
requiresRestart: true,
|
||||||
default: false,
|
default: false,
|
||||||
description: 'Enable planning features (Plan Mode and tools).',
|
description: 'Enable planning features (Plan Mode and tools).',
|
||||||
showInDialog: true,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
taskTracker: {
|
taskTracker: {
|
||||||
type: 'boolean',
|
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;
|
} as const satisfies SettingsSchema;
|
||||||
|
|
||||||
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
|
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ describe('SettingsDialog', () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Should wrap to last setting (without relying on exact bullet character)
|
// Should wrap to last setting (without relying on exact bullet character)
|
||||||
expect(lastFrame()).toContain('Hook Notifications');
|
expect(lastFrame()).toContain('Zed Integration');
|
||||||
});
|
});
|
||||||
|
|
||||||
unmount();
|
unmount();
|
||||||
@@ -753,6 +753,33 @@ describe('SettingsDialog', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Specific Settings Behavior', () => {
|
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 () => {
|
it('should show correct display values for settings with different states', async () => {
|
||||||
const settings = createMockSettings({
|
const settings = createMockSettings({
|
||||||
user: {
|
user: {
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ import {
|
|||||||
type SettingsValue,
|
type SettingsValue,
|
||||||
TOGGLE_TYPES,
|
TOGGLE_TYPES,
|
||||||
} from '../../config/settingsSchema.js';
|
} 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 { useSearchBuffer } from '../hooks/useSearchBuffer.js';
|
||||||
import {
|
import {
|
||||||
@@ -244,6 +249,14 @@ export function SettingsDialog({
|
|||||||
// The inline editor needs a string but non primitive settings like Arrays and Objects exist
|
// The inline editor needs a string but non primitive settings like Arrays and Objects exist
|
||||||
const editValue = getEditValue(type, rawValue);
|
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 {
|
return {
|
||||||
key,
|
key,
|
||||||
label: definition?.label || key,
|
label: definition?.label || key,
|
||||||
@@ -252,8 +265,10 @@ export function SettingsDialog({
|
|||||||
displayValue,
|
displayValue,
|
||||||
isGreyedOut,
|
isGreyedOut,
|
||||||
scopeMessage,
|
scopeMessage,
|
||||||
rawValue,
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
rawValue: rawValue as string | number | boolean | undefined,
|
||||||
editValue,
|
editValue,
|
||||||
|
stage,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [settingKeys, selectedScope, settings]);
|
}, [settingKeys, selectedScope, settings]);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||||
import { formatCommand } from '../../utils/keybindingUtils.js';
|
import { formatCommand } from '../../utils/keybindingUtils.js';
|
||||||
|
import { FeatureStage } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a single item in the settings dialog.
|
* Represents a single item in the settings dialog.
|
||||||
@@ -49,6 +50,8 @@ export interface SettingsDialogItem {
|
|||||||
rawValue?: SettingsValue;
|
rawValue?: SettingsValue;
|
||||||
/** Optional pre-formatted edit buffer value for complex types */
|
/** Optional pre-formatted edit buffer value for complex types */
|
||||||
editValue?: string;
|
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}
|
color={isActive ? theme.ui.focus : theme.text.primary}
|
||||||
>
|
>
|
||||||
{item.label}
|
{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 && (
|
{item.scopeMessage && (
|
||||||
<Text color={theme.text.secondary}>
|
<Text color={theme.text.secondary}>
|
||||||
{' '}
|
{' '}
|
||||||
|
|||||||
@@ -2391,7 +2391,102 @@ describe('Config setExperiments logging', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Availability Service Integration', () => {
|
describe('FeatureGate Integration', () => {
|
||||||
|
const baseParams: ConfigParameters = {
|
||||||
|
cwd: '/tmp',
|
||||||
|
targetDir: '/path/to/target',
|
||||||
|
debugMode: false,
|
||||||
|
sessionId: 'test-session-id',
|
||||||
|
model: 'gemini-pro',
|
||||||
|
usageStatisticsEnabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should initialize FeatureGate with defaults', () => {
|
||||||
|
const config = new Config(baseParams);
|
||||||
|
// Assuming 'plan' is Alpha and defaults to false
|
||||||
|
expect(config.isFeatureEnabled('plan')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect "features" setting', () => {
|
||||||
|
const params = {
|
||||||
|
...baseParams,
|
||||||
|
features: { plan: true },
|
||||||
|
};
|
||||||
|
const config = new Config(params);
|
||||||
|
expect(config.isFeatureEnabled('plan')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect "featureGates" CLI flag', () => {
|
||||||
|
const params = {
|
||||||
|
...baseParams,
|
||||||
|
featureGates: 'plan=true',
|
||||||
|
};
|
||||||
|
const config = new Config(params);
|
||||||
|
expect(config.isFeatureEnabled('plan')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect "featureGates" CLI flag overriding settings', () => {
|
||||||
|
const params = {
|
||||||
|
...baseParams,
|
||||||
|
features: { plan: false },
|
||||||
|
featureGates: 'plan=true',
|
||||||
|
};
|
||||||
|
const config = new Config(params);
|
||||||
|
expect(config.isFeatureEnabled('plan')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect legacy "experimental" settings if feature is not explicitly set', () => {
|
||||||
|
const params = {
|
||||||
|
...baseParams,
|
||||||
|
plan: true, // legacy experimental param for plan
|
||||||
|
};
|
||||||
|
const config = new Config(params);
|
||||||
|
expect(config.isFeatureEnabled('plan')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize "features" over legacy settings', () => {
|
||||||
|
const params = {
|
||||||
|
...baseParams,
|
||||||
|
plan: true, // legacy enabled
|
||||||
|
features: { plan: false }, // new disabled
|
||||||
|
};
|
||||||
|
const config = new Config(params);
|
||||||
|
expect(config.isFeatureEnabled('plan')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect stage-based toggles from settings', () => {
|
||||||
|
const params = {
|
||||||
|
...baseParams,
|
||||||
|
features: { allAlpha: true },
|
||||||
|
};
|
||||||
|
const config = new Config(params);
|
||||||
|
// 'plan' is Alpha
|
||||||
|
expect(config.isFeatureEnabled('plan')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve specific feature accessors using FeatureGate', () => {
|
||||||
|
const config = new Config({
|
||||||
|
...baseParams,
|
||||||
|
features: {
|
||||||
|
jitContext: true,
|
||||||
|
toolOutputMasking: false,
|
||||||
|
extensionManagement: true,
|
||||||
|
plan: true,
|
||||||
|
enableAgents: true,
|
||||||
|
zedIntegration: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.isJitContextEnabled()).toBe(true);
|
||||||
|
expect(config.getToolOutputMaskingEnabled()).toBe(false);
|
||||||
|
expect(config.getExtensionManagement()).toBe(true);
|
||||||
|
expect(config.isPlanEnabled()).toBe(true);
|
||||||
|
expect(config.isAgentsEnabled()).toBe(true);
|
||||||
|
expect(config.getExperimentalZedIntegration()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('refreshAuth', () => {
|
||||||
const baseModel = 'test-model';
|
const baseModel = 'test-model';
|
||||||
const baseParams: ConfigParameters = {
|
const baseParams: ConfigParameters = {
|
||||||
sessionId: 'test',
|
sessionId: 'test',
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ import { WorkspaceContext } from '../utils/workspaceContext.js';
|
|||||||
import { Storage } from './storage.js';
|
import { Storage } from './storage.js';
|
||||||
import type { ShellExecutionConfig } from '../services/shellExecutionService.js';
|
import type { ShellExecutionConfig } from '../services/shellExecutionService.js';
|
||||||
import { FileExclusions } from '../utils/ignorePatterns.js';
|
import { FileExclusions } from '../utils/ignorePatterns.js';
|
||||||
|
import { DefaultFeatureGate, type FeatureGate } from './features.js';
|
||||||
import { MessageBus } from '../confirmation-bus/message-bus.js';
|
import { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||||
import type { EventEmitter } from 'node:events';
|
import type { EventEmitter } from 'node:events';
|
||||||
import { PolicyEngine } from '../policy/policy-engine.js';
|
import { PolicyEngine } from '../policy/policy-engine.js';
|
||||||
@@ -584,6 +585,8 @@ export interface ConfigParameters {
|
|||||||
tracker?: boolean;
|
tracker?: boolean;
|
||||||
planSettings?: PlanSettings;
|
planSettings?: PlanSettings;
|
||||||
modelSteering?: boolean;
|
modelSteering?: boolean;
|
||||||
|
features?: Record<string, boolean>;
|
||||||
|
featureGates?: string;
|
||||||
onModelChange?: (model: string) => void;
|
onModelChange?: (model: string) => void;
|
||||||
mcpEnabled?: boolean;
|
mcpEnabled?: boolean;
|
||||||
extensionsEnabled?: boolean;
|
extensionsEnabled?: boolean;
|
||||||
@@ -731,6 +734,7 @@ export class Config implements McpContext {
|
|||||||
private readonly useAlternateBuffer: boolean;
|
private readonly useAlternateBuffer: boolean;
|
||||||
private shellExecutionConfig: ShellExecutionConfig;
|
private shellExecutionConfig: ShellExecutionConfig;
|
||||||
private readonly extensionManagement: boolean = true;
|
private readonly extensionManagement: boolean = true;
|
||||||
|
private readonly enablePromptCompletion: boolean = false;
|
||||||
private readonly truncateToolOutputThreshold: number;
|
private readonly truncateToolOutputThreshold: number;
|
||||||
private compressionTruncationCounter = 0;
|
private compressionTruncationCounter = 0;
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
@@ -791,7 +795,8 @@ export class Config implements McpContext {
|
|||||||
private disabledSkills: string[];
|
private disabledSkills: string[];
|
||||||
private readonly adminSkillsEnabled: boolean;
|
private readonly adminSkillsEnabled: boolean;
|
||||||
|
|
||||||
private readonly experimentalJitContext: boolean;
|
private readonly featureGate: FeatureGate;
|
||||||
|
|
||||||
private readonly disableLLMCorrection: boolean;
|
private readonly disableLLMCorrection: boolean;
|
||||||
private readonly planEnabled: boolean;
|
private readonly planEnabled: boolean;
|
||||||
private readonly trackerEnabled: boolean;
|
private readonly trackerEnabled: boolean;
|
||||||
@@ -881,7 +886,6 @@ export class Config implements McpContext {
|
|||||||
this.model = params.model;
|
this.model = params.model;
|
||||||
this.disableLoopDetection = params.disableLoopDetection ?? false;
|
this.disableLoopDetection = params.disableLoopDetection ?? false;
|
||||||
this._activeModel = params.model;
|
this._activeModel = params.model;
|
||||||
this.enableAgents = params.enableAgents ?? false;
|
|
||||||
this.agents = params.agents ?? {};
|
this.agents = params.agents ?? {};
|
||||||
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
|
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
|
||||||
this.planEnabled = params.plan ?? false;
|
this.planEnabled = params.plan ?? false;
|
||||||
@@ -891,6 +895,41 @@ export class Config implements McpContext {
|
|||||||
this.skillsSupport = params.skillsSupport ?? true;
|
this.skillsSupport = params.skillsSupport ?? true;
|
||||||
this.disabledSkills = params.disabledSkills ?? [];
|
this.disabledSkills = params.disabledSkills ?? [];
|
||||||
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
|
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
|
||||||
|
|
||||||
|
// Initialize FeatureGate with precedence:
|
||||||
|
// 1. CLI Flags (params.featureGates)
|
||||||
|
// 2. Env Var (GEMINI_FEATURE_GATES)
|
||||||
|
// 3. User Settings (params.features)
|
||||||
|
// 4. Legacy Experimental Settings
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
if (params.features) {
|
||||||
|
gate.setFromMap(params.features);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map legacy experimental flags to features if not already set
|
||||||
|
const legacyMap: Record<string, boolean | undefined> = {
|
||||||
|
toolOutputMasking: params.toolOutputMasking?.enabled,
|
||||||
|
enableAgents: params.enableAgents,
|
||||||
|
extensionManagement: params.extensionManagement,
|
||||||
|
plan: params.plan,
|
||||||
|
jitContext: params.experimentalJitContext,
|
||||||
|
zedIntegration: params.experimentalZedIntegration,
|
||||||
|
};
|
||||||
|
for (const [key, value] of Object.entries(legacyMap)) {
|
||||||
|
if (value !== undefined && params.features?.[key] === undefined) {
|
||||||
|
gate.setFromMap({ [key]: value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const envGates = process.env['GEMINI_FEATURE_GATES'];
|
||||||
|
if (envGates) {
|
||||||
|
gate.set(envGates);
|
||||||
|
}
|
||||||
|
if (params.featureGates) {
|
||||||
|
gate.set(params.featureGates);
|
||||||
|
}
|
||||||
|
this.featureGate = gate;
|
||||||
|
|
||||||
this.modelAvailabilityService = new ModelAvailabilityService();
|
this.modelAvailabilityService = new ModelAvailabilityService();
|
||||||
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||||
this.modelSteering = params.modelSteering ?? false;
|
this.modelSteering = params.modelSteering ?? false;
|
||||||
@@ -959,7 +998,6 @@ export class Config implements McpContext {
|
|||||||
params.enableShellOutputEfficiency ?? true;
|
params.enableShellOutputEfficiency ?? true;
|
||||||
this.shellToolInactivityTimeout =
|
this.shellToolInactivityTimeout =
|
||||||
(params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes
|
(params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes
|
||||||
this.extensionManagement = params.extensionManagement ?? true;
|
|
||||||
this.enableExtensionReloading = params.enableExtensionReloading ?? false;
|
this.enableExtensionReloading = params.enableExtensionReloading ?? false;
|
||||||
this.storage = new Storage(this.targetDir, this.sessionId);
|
this.storage = new Storage(this.targetDir, this.sessionId);
|
||||||
this.storage.setCustomPlansDir(params.planSettings?.directory);
|
this.storage.setCustomPlansDir(params.planSettings?.directory);
|
||||||
@@ -1092,6 +1130,13 @@ export class Config implements McpContext {
|
|||||||
return this.initialized;
|
return this.initialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the feature is enabled.
|
||||||
|
*/
|
||||||
|
isFeatureEnabled(key: string): boolean {
|
||||||
|
return this.featureGate.enabled(key);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dedups initialization requests using a shared promise that is only resolved
|
* Dedups initialization requests using a shared promise that is only resolved
|
||||||
* once.
|
* once.
|
||||||
@@ -1163,7 +1208,7 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.interactive || this.acpMode) {
|
if (!this.interactive || this.acpMode || this.getExperimentalZedIntegration()) {
|
||||||
await this.mcpInitializationPromise;
|
await this.mcpInitializationPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1193,7 +1238,7 @@ export class Config implements McpContext {
|
|||||||
await this.hookSystem.initialize();
|
await this.hookSystem.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.experimentalJitContext) {
|
if (this.isFeatureEnabled('jitContext')) {
|
||||||
this.contextManager = new ContextManager(this);
|
this.contextManager = new ContextManager(this);
|
||||||
await this.contextManager.refresh();
|
await this.contextManager.refresh();
|
||||||
}
|
}
|
||||||
@@ -1853,7 +1898,7 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUserMemory(): string | HierarchicalMemory {
|
getUserMemory(): string | HierarchicalMemory {
|
||||||
if (this.experimentalJitContext && this.contextManager) {
|
if (this.isFeatureEnabled('jitContext') && this.contextManager) {
|
||||||
return {
|
return {
|
||||||
global: this.contextManager.getGlobalMemory(),
|
global: this.contextManager.getGlobalMemory(),
|
||||||
extension: this.contextManager.getExtensionMemory(),
|
extension: this.contextManager.getExtensionMemory(),
|
||||||
@@ -1867,7 +1912,7 @@ export class Config implements McpContext {
|
|||||||
* Refreshes the MCP context, including memory, tools, and system instructions.
|
* Refreshes the MCP context, including memory, tools, and system instructions.
|
||||||
*/
|
*/
|
||||||
async refreshMcpContext(): Promise<void> {
|
async refreshMcpContext(): Promise<void> {
|
||||||
if (this.experimentalJitContext && this.contextManager) {
|
if (this.isFeatureEnabled('jitContext') && this.contextManager) {
|
||||||
await this.contextManager.refresh();
|
await this.contextManager.refresh();
|
||||||
} else {
|
} else {
|
||||||
const { refreshServerHierarchicalMemory } = await import(
|
const { refreshServerHierarchicalMemory } = await import(
|
||||||
@@ -1898,7 +1943,7 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isJitContextEnabled(): boolean {
|
isJitContextEnabled(): boolean {
|
||||||
return this.experimentalJitContext;
|
return this.isFeatureEnabled('jitContext');
|
||||||
}
|
}
|
||||||
|
|
||||||
isModelSteeringEnabled(): boolean {
|
isModelSteeringEnabled(): boolean {
|
||||||
@@ -1906,7 +1951,7 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToolOutputMaskingEnabled(): boolean {
|
getToolOutputMaskingEnabled(): boolean {
|
||||||
return this.toolOutputMasking.enabled;
|
return this.isFeatureEnabled('toolOutputMasking');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getToolOutputMaskingConfig(): Promise<ToolOutputMaskingConfig> {
|
async getToolOutputMaskingConfig(): Promise<ToolOutputMaskingConfig> {
|
||||||
@@ -1930,7 +1975,7 @@ export class Config implements McpContext {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: this.toolOutputMasking.enabled,
|
enabled: this.getToolOutputMaskingEnabled(),
|
||||||
toolProtectionThreshold:
|
toolProtectionThreshold:
|
||||||
parsedProtection !== undefined && !isNaN(parsedProtection)
|
parsedProtection !== undefined && !isNaN(parsedProtection)
|
||||||
? parsedProtection
|
? parsedProtection
|
||||||
@@ -1945,7 +1990,7 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGeminiMdFileCount(): number {
|
getGeminiMdFileCount(): number {
|
||||||
if (this.experimentalJitContext && this.contextManager) {
|
if (this.isFeatureEnabled('jitContext') && this.contextManager) {
|
||||||
return this.contextManager.getLoadedPaths().size;
|
return this.contextManager.getLoadedPaths().size;
|
||||||
}
|
}
|
||||||
return this.geminiMdFileCount;
|
return this.geminiMdFileCount;
|
||||||
@@ -1956,7 +2001,7 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGeminiMdFilePaths(): string[] {
|
getGeminiMdFilePaths(): string[] {
|
||||||
if (this.experimentalJitContext && this.contextManager) {
|
if (this.isFeatureEnabled('jitContext') && this.contextManager) {
|
||||||
return Array.from(this.contextManager.getLoadedPaths());
|
return Array.from(this.contextManager.getLoadedPaths());
|
||||||
}
|
}
|
||||||
return this.geminiMdFilePaths;
|
return this.geminiMdFilePaths;
|
||||||
@@ -2040,6 +2085,42 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes enter/exit plan mode tools based on current mode.
|
||||||
|
*/
|
||||||
|
syncPlanModeTools(): void {
|
||||||
|
const isPlanMode = this.getApprovalMode() === ApprovalMode.PLAN;
|
||||||
|
const registry = this.getToolRegistry();
|
||||||
|
|
||||||
|
if (isPlanMode) {
|
||||||
|
if (registry.getTool(ENTER_PLAN_MODE_TOOL_NAME)) {
|
||||||
|
registry.unregisterTool(ENTER_PLAN_MODE_TOOL_NAME);
|
||||||
|
}
|
||||||
|
if (!registry.getTool(EXIT_PLAN_MODE_TOOL_NAME)) {
|
||||||
|
registry.registerTool(new ExitPlanModeTool(this, this.messageBus));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (registry.getTool(EXIT_PLAN_MODE_TOOL_NAME)) {
|
||||||
|
registry.unregisterTool(EXIT_PLAN_MODE_TOOL_NAME);
|
||||||
|
}
|
||||||
|
if (this.isPlanEnabled()) {
|
||||||
|
if (!registry.getTool(ENTER_PLAN_MODE_TOOL_NAME)) {
|
||||||
|
registry.registerTool(new EnterPlanModeTool(this, this.messageBus));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (registry.getTool(ENTER_PLAN_MODE_TOOL_NAME)) {
|
||||||
|
registry.unregisterTool(ENTER_PLAN_MODE_TOOL_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.geminiClient?.isInitialized()) {
|
||||||
|
this.geminiClient.setTools().catch((err) => {
|
||||||
|
debugLogger.error('Failed to update tools', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs the duration of the current approval mode.
|
* Logs the duration of the current approval mode.
|
||||||
*/
|
*/
|
||||||
@@ -2246,6 +2327,10 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExperimentalZedIntegration(): boolean {
|
||||||
|
return this.isFeatureEnabled('zedIntegration');
|
||||||
|
}
|
||||||
|
|
||||||
getListExtensions(): boolean {
|
getListExtensions(): boolean {
|
||||||
return this.listExtensions;
|
return this.listExtensions;
|
||||||
}
|
}
|
||||||
@@ -2259,7 +2344,7 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getExtensionManagement(): boolean {
|
getExtensionManagement(): boolean {
|
||||||
return this.extensionManagement;
|
return this.isFeatureEnabled('extensionManagement');
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtensions(): GeminiCLIExtension[] {
|
getExtensions(): GeminiCLIExtension[] {
|
||||||
@@ -2285,7 +2370,7 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPlanEnabled(): boolean {
|
isPlanEnabled(): boolean {
|
||||||
return this.planEnabled;
|
return this.isFeatureEnabled('plan');
|
||||||
}
|
}
|
||||||
|
|
||||||
isTrackerEnabled(): boolean {
|
isTrackerEnabled(): boolean {
|
||||||
@@ -2305,7 +2390,7 @@ export class Config implements McpContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isAgentsEnabled(): boolean {
|
isAgentsEnabled(): boolean {
|
||||||
return this.enableAgents;
|
return this.isFeatureEnabled('enableAgents');
|
||||||
}
|
}
|
||||||
|
|
||||||
isEventDrivenSchedulerEnabled(): boolean {
|
isEventDrivenSchedulerEnabled(): boolean {
|
||||||
|
|||||||
@@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { it, expect, describe, vi } from 'vitest';
|
||||||
|
import { DefaultFeatureGate, FeatureStage } from './features.js';
|
||||||
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
|
|
||||||
|
describe('FeatureGate', () => {
|
||||||
|
it('should resolve default values', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
testAlpha: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
testBeta: [
|
||||||
|
{ default: true, lockToDefault: false, preRelease: FeatureStage.Beta },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(gate.enabled('testAlpha')).toBe(false);
|
||||||
|
expect(gate.enabled('testBeta')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should infer default values from stage', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
autoAlpha: [{ lockToDefault: false, preRelease: FeatureStage.Alpha }],
|
||||||
|
autoBeta: [{ lockToDefault: false, preRelease: FeatureStage.Beta }],
|
||||||
|
autoGA: [{ lockToDefault: true, preRelease: FeatureStage.GA }],
|
||||||
|
autoDeprecated: [
|
||||||
|
{ lockToDefault: false, preRelease: FeatureStage.Deprecated },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(gate.enabled('autoAlpha')).toBe(false);
|
||||||
|
expect(gate.enabled('autoBeta')).toBe(true);
|
||||||
|
expect(gate.enabled('autoGA')).toBe(true);
|
||||||
|
expect(gate.enabled('autoDeprecated')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should infer lockToDefault from stage', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
autoLockedGA: [{ preRelease: FeatureStage.GA }],
|
||||||
|
autoUnlockedAlpha: [{ preRelease: FeatureStage.Alpha }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attempt to disable both
|
||||||
|
gate.setFromMap({ autoLockedGA: false, autoUnlockedAlpha: true });
|
||||||
|
|
||||||
|
// GA should remain enabled (locked)
|
||||||
|
expect(gate.enabled('autoLockedGA')).toBe(true);
|
||||||
|
// Alpha should respect override (unlocked)
|
||||||
|
expect(gate.enabled('autoUnlockedAlpha')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect explicit default even if stage default differs', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
offBeta: [
|
||||||
|
{ default: false, lockToDefault: false, preRelease: FeatureStage.Beta },
|
||||||
|
],
|
||||||
|
onAlpha: [
|
||||||
|
{ default: true, lockToDefault: false, preRelease: FeatureStage.Alpha },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(gate.enabled('offBeta')).toBe(false);
|
||||||
|
expect(gate.enabled('onAlpha')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect manual overrides', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
testAlpha: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
gate.setFromMap({ testAlpha: true });
|
||||||
|
expect(gate.enabled('testAlpha')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect lockToDefault', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
testGA: [
|
||||||
|
{ default: true, lockToDefault: true, preRelease: FeatureStage.GA },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// Attempt to disable GA feature
|
||||||
|
gate.setFromMap({ testGA: false });
|
||||||
|
expect(gate.enabled('testGA')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect allAlpha/allBeta toggles', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
alpha1: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
alpha2: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
beta1: [
|
||||||
|
{ default: true, lockToDefault: false, preRelease: FeatureStage.Beta },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable all alpha, disable all beta
|
||||||
|
gate.setFromMap({ allAlpha: true, allBeta: false });
|
||||||
|
expect(gate.enabled('alpha1')).toBe(true);
|
||||||
|
expect(gate.enabled('alpha2')).toBe(true);
|
||||||
|
expect(gate.enabled('beta1')).toBe(false);
|
||||||
|
|
||||||
|
// Individual override should still win
|
||||||
|
gate.setFromMap({ alpha1: false });
|
||||||
|
expect(gate.enabled('alpha1')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse comma-separated strings', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
feat1: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
feat2: [
|
||||||
|
{ default: true, lockToDefault: false, preRelease: FeatureStage.Beta },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
gate.set('feat1=true,feat2=false');
|
||||||
|
expect(gate.enabled('feat1')).toBe(true);
|
||||||
|
expect(gate.enabled('feat2')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle case-insensitive boolean values in set', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
feat1: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
feat2: [
|
||||||
|
{ default: true, lockToDefault: false, preRelease: FeatureStage.Beta },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
gate.set('feat1=TRUE,feat2=FaLsE');
|
||||||
|
expect(gate.enabled('feat1')).toBe(true);
|
||||||
|
expect(gate.enabled('feat2')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore whitespace in set', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
feat1: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
gate.set(' feat1 = true ');
|
||||||
|
expect(gate.enabled('feat1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default if feature is unknown', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
// unknownFeature is not added
|
||||||
|
expect(gate.enabled('unknownFeature')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect precedence: Lock > Override > Stage > Default', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
// Locked GA feature
|
||||||
|
featLocked: [
|
||||||
|
{ default: true, lockToDefault: true, preRelease: FeatureStage.GA },
|
||||||
|
],
|
||||||
|
// Alpha feature
|
||||||
|
featAlpha: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Lock wins over override
|
||||||
|
gate.setFromMap({ featLocked: false });
|
||||||
|
expect(gate.enabled('featLocked')).toBe(true);
|
||||||
|
|
||||||
|
// 2. Override wins over Stage
|
||||||
|
gate.setFromMap({ allAlpha: true, featAlpha: false });
|
||||||
|
expect(gate.enabled('featAlpha')).toBe(false);
|
||||||
|
|
||||||
|
// 3. Stage wins over Default
|
||||||
|
gate.setFromMap({
|
||||||
|
allAlpha: true,
|
||||||
|
featAlpha: undefined as unknown as boolean,
|
||||||
|
}); // Removing specific override effectively
|
||||||
|
// Re-create to clear overrides map for cleaner test
|
||||||
|
const gate2 = DefaultFeatureGate.deepCopy();
|
||||||
|
gate2.add({
|
||||||
|
featAlpha: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
gate2.setFromMap({ allAlpha: true });
|
||||||
|
expect(gate2.enabled('featAlpha')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the latest feature spec', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
evolvedFeat: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
since: '1.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: true,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Beta,
|
||||||
|
since: '1.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// Should use the last spec (Beta, default true)
|
||||||
|
expect(gate.enabled('evolvedFeat')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log warning when using deprecated feature only once', () => {
|
||||||
|
const warnSpy = vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
gate.add({
|
||||||
|
deprecatedFeat: [
|
||||||
|
{
|
||||||
|
default: false,
|
||||||
|
lockToDefault: false,
|
||||||
|
preRelease: FeatureStage.Deprecated,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
gate.setFromMap({ deprecatedFeat: true });
|
||||||
|
expect(gate.enabled('deprecatedFeat')).toBe(true);
|
||||||
|
expect(gate.enabled('deprecatedFeat')).toBe(true); // Call again
|
||||||
|
|
||||||
|
// Should only be called once
|
||||||
|
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Feature "deprecatedFeat" is deprecated'),
|
||||||
|
);
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform deep copy of specs', () => {
|
||||||
|
const gate = DefaultFeatureGate.deepCopy();
|
||||||
|
const featKey = 'copiedFeat';
|
||||||
|
const initialSpecs = [{ preRelease: FeatureStage.Alpha }];
|
||||||
|
gate.add({ [featKey]: initialSpecs });
|
||||||
|
|
||||||
|
const copy = gate.deepCopy();
|
||||||
|
|
||||||
|
// Modifying original spec array should not affect copy if it was truly deep copied
|
||||||
|
// (though our implementation clones the array, not the spec objects, which is usually enough for this use case)
|
||||||
|
gate.add({
|
||||||
|
[featKey]: [{ preRelease: FeatureStage.Beta }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(gate.enabled(featKey)).toBe(true); // Beta (default true)
|
||||||
|
expect(copy.enabled(featKey)).toBe(false); // Alpha (default false)
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeatureStage indicates the maturity level of a feature.
|
||||||
|
* Strictly aligned with Kubernetes Feature Gates.
|
||||||
|
*/
|
||||||
|
export enum FeatureStage {
|
||||||
|
/**
|
||||||
|
* Alpha features are disabled by default and may be unstable.
|
||||||
|
*/
|
||||||
|
Alpha = 'ALPHA',
|
||||||
|
/**
|
||||||
|
* Beta features are enabled by default and are considered stable.
|
||||||
|
*/
|
||||||
|
Beta = 'BETA',
|
||||||
|
/**
|
||||||
|
* GA features are stable and locked to enabled.
|
||||||
|
*/
|
||||||
|
GA = 'GA',
|
||||||
|
/**
|
||||||
|
* Deprecated features are scheduled for removal.
|
||||||
|
*/
|
||||||
|
Deprecated = 'DEPRECATED',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeatureSpec defines the behavior and metadata of a feature at a specific version.
|
||||||
|
*/
|
||||||
|
export interface FeatureSpec {
|
||||||
|
/**
|
||||||
|
* Default enablement state.
|
||||||
|
* If not provided, defaults to:
|
||||||
|
* - Alpha: false
|
||||||
|
* - Beta: true
|
||||||
|
* - GA: true
|
||||||
|
* - Deprecated: false
|
||||||
|
*/
|
||||||
|
default?: boolean;
|
||||||
|
/**
|
||||||
|
* If true, the feature cannot be changed from its default value.
|
||||||
|
* Defaults to:
|
||||||
|
* - GA: true
|
||||||
|
* - Others: false
|
||||||
|
*/
|
||||||
|
lockToDefault?: boolean;
|
||||||
|
/**
|
||||||
|
* The maturity stage of the feature.
|
||||||
|
*/
|
||||||
|
preRelease: FeatureStage;
|
||||||
|
/**
|
||||||
|
* The version since this spec became valid.
|
||||||
|
*/
|
||||||
|
since?: string;
|
||||||
|
/**
|
||||||
|
* The version until which this spec is valid or scheduled for removal.
|
||||||
|
*/
|
||||||
|
until?: string;
|
||||||
|
/**
|
||||||
|
* Description of the feature.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeatureGate provides a read-only interface to query feature status.
|
||||||
|
*/
|
||||||
|
export interface FeatureGate {
|
||||||
|
/**
|
||||||
|
* Returns true if the feature is enabled.
|
||||||
|
*/
|
||||||
|
enabled(key: string): boolean;
|
||||||
|
/**
|
||||||
|
* Returns all known feature keys.
|
||||||
|
*/
|
||||||
|
knownFeatures(): string[];
|
||||||
|
/**
|
||||||
|
* Returns a mutable copy of the current gate.
|
||||||
|
*/
|
||||||
|
deepCopy(): MutableFeatureGate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MutableFeatureGate allows registering and configuring features.
|
||||||
|
*/
|
||||||
|
export interface MutableFeatureGate extends FeatureGate {
|
||||||
|
/**
|
||||||
|
* Adds new features or updates existing ones with versioned specs.
|
||||||
|
*/
|
||||||
|
add(features: Record<string, FeatureSpec[]>): void;
|
||||||
|
/**
|
||||||
|
* Sets feature states from a comma-separated string (e.g., "Foo=true,Bar=false").
|
||||||
|
*/
|
||||||
|
set(instance: string): void;
|
||||||
|
/**
|
||||||
|
* Sets feature states from a map.
|
||||||
|
*/
|
||||||
|
setFromMap(m: Record<string, boolean>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FeatureGateImpl implements MutableFeatureGate {
|
||||||
|
private specs: Map<string, FeatureSpec[]> = new Map();
|
||||||
|
private overrides: Map<string, boolean> = new Map();
|
||||||
|
private warnedFeatures: Set<string> = new Set();
|
||||||
|
|
||||||
|
add(features: Record<string, FeatureSpec[]>): void {
|
||||||
|
for (const [key, specs] of Object.entries(features)) {
|
||||||
|
this.specs.set(key, specs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(instance: string): void {
|
||||||
|
const pairs = instance.split(',');
|
||||||
|
for (const pair of pairs) {
|
||||||
|
const eqIndex = pair.indexOf('=');
|
||||||
|
if (eqIndex !== -1) {
|
||||||
|
const key = pair.slice(0, eqIndex).trim();
|
||||||
|
const value = pair.slice(eqIndex + 1).trim();
|
||||||
|
if (key) {
|
||||||
|
this.overrides.set(key, value.toLowerCase() === 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFromMap(m: Record<string, boolean>): void {
|
||||||
|
for (const [key, value] of Object.entries(m)) {
|
||||||
|
this.overrides.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled(key: string): boolean {
|
||||||
|
const specs = this.specs.get(key);
|
||||||
|
if (!specs || specs.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest spec (for now, just the last one in the array)
|
||||||
|
const latestSpec = specs[specs.length - 1];
|
||||||
|
|
||||||
|
const isLocked =
|
||||||
|
latestSpec.lockToDefault ?? latestSpec.preRelease === FeatureStage.GA;
|
||||||
|
|
||||||
|
if (isLocked) {
|
||||||
|
return latestSpec.default ?? true; // Locked features (GA) must be enabled unless explicitly disabled (rare)
|
||||||
|
}
|
||||||
|
|
||||||
|
const override = this.overrides.get(key);
|
||||||
|
if (override !== undefined) {
|
||||||
|
if (
|
||||||
|
latestSpec.preRelease === FeatureStage.Deprecated &&
|
||||||
|
!this.warnedFeatures.has(key)
|
||||||
|
) {
|
||||||
|
debugLogger.warn(
|
||||||
|
`[WARNING] Feature "${key}" is deprecated and will be removed in a future release.`,
|
||||||
|
);
|
||||||
|
this.warnedFeatures.add(key);
|
||||||
|
}
|
||||||
|
return override;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle stage-wide defaults if set (e.g., allAlpha, allBeta)
|
||||||
|
if (latestSpec.preRelease === FeatureStage.Alpha) {
|
||||||
|
const allAlpha = this.overrides.get('allAlpha');
|
||||||
|
if (allAlpha !== undefined) return allAlpha;
|
||||||
|
}
|
||||||
|
if (latestSpec.preRelease === FeatureStage.Beta) {
|
||||||
|
const allBeta = this.overrides.get('allBeta');
|
||||||
|
if (allBeta !== undefined) return allBeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestSpec.default !== undefined) {
|
||||||
|
return latestSpec.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-default based on stage
|
||||||
|
return (
|
||||||
|
latestSpec.preRelease === FeatureStage.Beta ||
|
||||||
|
latestSpec.preRelease === FeatureStage.GA
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
knownFeatures(): string[] {
|
||||||
|
return Array.from(this.specs.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
deepCopy(): MutableFeatureGate {
|
||||||
|
const copy = new FeatureGateImpl();
|
||||||
|
copy.specs = new Map(
|
||||||
|
Array.from(this.specs.entries()).map(([k, v]) => [k, [...v]]),
|
||||||
|
);
|
||||||
|
copy.overrides = new Map(this.overrides);
|
||||||
|
// warnedFeatures are not copied, we want to warn again in a new context if needed
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global default feature gate.
|
||||||
|
*/
|
||||||
|
export const DefaultFeatureGate: MutableFeatureGate = new FeatureGateImpl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry of core features.
|
||||||
|
*/
|
||||||
|
export const FeatureDefinitions: Record<string, FeatureSpec[]> = {
|
||||||
|
toolOutputMasking: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Beta,
|
||||||
|
since: '0.30.0',
|
||||||
|
description: 'Enables tool output masking to save tokens.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
enableAgents: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
since: '0.30.0',
|
||||||
|
description: 'Enable local and remote subagents.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionManagement: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Beta,
|
||||||
|
since: '0.30.0',
|
||||||
|
description: 'Enable extension management features.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionConfig: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Beta,
|
||||||
|
since: '0.30.0',
|
||||||
|
description: 'Enable requesting and fetching of extension settings.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionRegistry: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
since: '0.30.0',
|
||||||
|
description: 'Enable extension registry explore UI.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensionReloading: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
since: '0.30.0',
|
||||||
|
description:
|
||||||
|
'Enables extension loading/unloading within the CLI session.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
jitContext: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
since: '0.30.0',
|
||||||
|
description: 'Enable Just-In-Time (JIT) context loading.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
useOSC52Paste: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
since: '0.30.0',
|
||||||
|
description: 'Use OSC 52 sequence for pasting.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plan: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
since: '0.30.0',
|
||||||
|
description: 'Enable planning features (Plan Mode and tools).',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
zedIntegration: [
|
||||||
|
{
|
||||||
|
preRelease: FeatureStage.Alpha,
|
||||||
|
since: '0.30.0',
|
||||||
|
description: 'Enable Zed integration.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register core features
|
||||||
|
DefaultFeatureGate.add(FeatureDefinitions);
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
// Export config
|
// Export config
|
||||||
export * from './config/config.js';
|
export * from './config/config.js';
|
||||||
export * from './config/memory.js';
|
export * from './config/memory.js';
|
||||||
|
export * from './config/features.js';
|
||||||
export * from './config/defaultModelConfigs.js';
|
export * from './config/defaultModelConfigs.js';
|
||||||
export * from './config/models.js';
|
export * from './config/models.js';
|
||||||
export * from './config/constants.js';
|
export * from './config/constants.js';
|
||||||
|
|||||||
@@ -1611,8 +1611,8 @@
|
|||||||
},
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"title": "Experimental",
|
"title": "Experimental",
|
||||||
"description": "Setting to enable experimental features",
|
"description": "DEPRECATED: Use the \"features\" object instead. Setting to enable experimental features",
|
||||||
"markdownDescription": "Setting to enable experimental features\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `{}`",
|
"markdownDescription": "DEPRECATED: Use the \"features\" object instead. Setting to enable experimental features\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `{}`",
|
||||||
"default": {},
|
"default": {},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2040,6 +2040,100 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"title": "Features",
|
||||||
|
"description": "Feature Lifecycle Management settings.",
|
||||||
|
"markdownDescription": "Feature Lifecycle Management settings.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `{}`",
|
||||||
|
"default": {},
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"allAlpha": {
|
||||||
|
"title": "Enable all Alpha features",
|
||||||
|
"description": "Enable all Alpha features by default.",
|
||||||
|
"markdownDescription": "Enable all Alpha features by default.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"allBeta": {
|
||||||
|
"title": "Enable all Beta features",
|
||||||
|
"description": "Enable all Beta features by default.",
|
||||||
|
"markdownDescription": "Enable all Beta features by default.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `true`",
|
||||||
|
"default": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"toolOutputMasking": {
|
||||||
|
"title": "Tool Output Masking",
|
||||||
|
"description": "Enables tool output masking to save tokens.",
|
||||||
|
"markdownDescription": "Enables tool output masking to save tokens.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `true`",
|
||||||
|
"default": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"enableAgents": {
|
||||||
|
"title": "Enable Agents",
|
||||||
|
"description": "Enable local and remote subagents.",
|
||||||
|
"markdownDescription": "Enable local and remote subagents.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"extensionManagement": {
|
||||||
|
"title": "Extension Management",
|
||||||
|
"description": "Enable extension management features.",
|
||||||
|
"markdownDescription": "Enable extension management features.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `true`",
|
||||||
|
"default": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"extensionConfig": {
|
||||||
|
"title": "Extension Configuration",
|
||||||
|
"description": "Enable requesting and fetching of extension settings.",
|
||||||
|
"markdownDescription": "Enable requesting and fetching of extension settings.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `true`",
|
||||||
|
"default": true,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"extensionRegistry": {
|
||||||
|
"title": "Extension Registry Explore UI",
|
||||||
|
"description": "Enable extension registry explore UI.",
|
||||||
|
"markdownDescription": "Enable extension registry explore UI.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"extensionReloading": {
|
||||||
|
"title": "Extension Reloading",
|
||||||
|
"description": "Enables extension loading/unloading within the CLI session.",
|
||||||
|
"markdownDescription": "Enables extension loading/unloading within the CLI session.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"jitContext": {
|
||||||
|
"title": "JIT Context Loading",
|
||||||
|
"description": "Enable Just-In-Time (JIT) context loading.",
|
||||||
|
"markdownDescription": "Enable Just-In-Time (JIT) context loading.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"useOSC52Paste": {
|
||||||
|
"title": "Use OSC 52 Paste",
|
||||||
|
"description": "Use OSC 52 sequence for pasting.",
|
||||||
|
"markdownDescription": "Use OSC 52 sequence for pasting.\n\n- Category: `Features`\n- Requires restart: `no`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"plan": {
|
||||||
|
"title": "Plan Mode",
|
||||||
|
"description": "Enable planning features (Plan Mode and tools).",
|
||||||
|
"markdownDescription": "Enable planning features (Plan Mode and tools).\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"zedIntegration": {
|
||||||
|
"title": "Zed Integration",
|
||||||
|
"description": "Enable Zed integration.",
|
||||||
|
"markdownDescription": "Enable Zed integration.\n\n- Category: `Features`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ import type {
|
|||||||
SettingsSchemaType,
|
SettingsSchemaType,
|
||||||
} from '../packages/cli/src/config/settingsSchema.js';
|
} from '../packages/cli/src/config/settingsSchema.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FeatureDefinitions,
|
||||||
|
FeatureStage,
|
||||||
|
} from '../packages/core/src/config/features.js';
|
||||||
|
|
||||||
const START_MARKER = '<!-- SETTINGS-AUTOGEN:START -->';
|
const START_MARKER = '<!-- SETTINGS-AUTOGEN:START -->';
|
||||||
const END_MARKER = '<!-- SETTINGS-AUTOGEN:END -->';
|
const END_MARKER = '<!-- SETTINGS-AUTOGEN:END -->';
|
||||||
|
|
||||||
@@ -36,6 +41,7 @@ interface DocEntry {
|
|||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
requiresRestart: boolean;
|
requiresRestart: boolean;
|
||||||
enumValues?: string[];
|
enumValues?: string[];
|
||||||
|
stage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function main(argv = process.argv.slice(2)) {
|
export async function main(argv = process.argv.slice(2)) {
|
||||||
@@ -49,6 +55,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|||||||
);
|
);
|
||||||
const docPath = path.join(repoRoot, 'docs/reference/configuration.md');
|
const docPath = path.join(repoRoot, 'docs/reference/configuration.md');
|
||||||
const cliSettingsDocPath = path.join(repoRoot, 'docs/cli/settings.md');
|
const cliSettingsDocPath = path.join(repoRoot, 'docs/cli/settings.md');
|
||||||
|
const featureGatesDocPath = path.join(
|
||||||
|
repoRoot,
|
||||||
|
'docs/cli/feature-lifecycle.md',
|
||||||
|
);
|
||||||
|
|
||||||
const { getSettingsSchema } = await loadSettingsSchemaModule();
|
const { getSettingsSchema } = await loadSettingsSchemaModule();
|
||||||
const schema = getSettingsSchema();
|
const schema = getSettingsSchema();
|
||||||
@@ -59,21 +69,31 @@ export async function main(argv = process.argv.slice(2)) {
|
|||||||
|
|
||||||
const generatedBlock = renderSections(allSettingsSections);
|
const generatedBlock = renderSections(allSettingsSections);
|
||||||
const generatedTableBlock = renderTableSections(filteredSettingsSections);
|
const generatedTableBlock = renderTableSections(filteredSettingsSections);
|
||||||
|
const generatedFeatureGatesBlock = renderFeatureGatesTable();
|
||||||
|
|
||||||
await updateFile(docPath, generatedBlock, checkOnly);
|
await updateFile(docPath, generatedBlock, checkOnly);
|
||||||
await updateFile(cliSettingsDocPath, generatedTableBlock, checkOnly);
|
await updateFile(cliSettingsDocPath, generatedTableBlock, checkOnly);
|
||||||
|
await updateFile(
|
||||||
|
featureGatesDocPath,
|
||||||
|
generatedFeatureGatesBlock,
|
||||||
|
checkOnly,
|
||||||
|
'<!-- FEATURES-AUTOGEN:START -->',
|
||||||
|
'<!-- FEATURES-AUTOGEN:END -->',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateFile(
|
async function updateFile(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
newContent: string,
|
newContent: string,
|
||||||
checkOnly: boolean,
|
checkOnly: boolean,
|
||||||
|
startMarker = START_MARKER,
|
||||||
|
endMarker = END_MARKER,
|
||||||
) {
|
) {
|
||||||
const doc = await readFile(filePath, 'utf8');
|
const doc = await readFile(filePath, 'utf8');
|
||||||
const injectedDoc = injectBetweenMarkers({
|
const injectedDoc = injectBetweenMarkers({
|
||||||
document: doc,
|
document: doc,
|
||||||
startMarker: START_MARKER,
|
startMarker,
|
||||||
endMarker: END_MARKER,
|
endMarker,
|
||||||
newContent: newContent,
|
newContent: newContent,
|
||||||
paddingBefore: '\n',
|
paddingBefore: '\n',
|
||||||
paddingAfter: '\n',
|
paddingAfter: '\n',
|
||||||
@@ -142,19 +162,31 @@ function collectEntries(
|
|||||||
sections.set(sectionKey, []);
|
sections.set(sectionKey, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let defaultValue = definition.default;
|
||||||
|
let stage: string | undefined;
|
||||||
|
if (sectionKey === 'features' && FeatureDefinitions[key]) {
|
||||||
|
const specs = FeatureDefinitions[key];
|
||||||
|
const latest = specs[specs.length - 1];
|
||||||
|
stage = latest.preRelease;
|
||||||
|
defaultValue =
|
||||||
|
latest.default ??
|
||||||
|
(stage === FeatureStage.Beta || stage === FeatureStage.GA);
|
||||||
|
}
|
||||||
|
|
||||||
sections.get(sectionKey)!.push({
|
sections.get(sectionKey)!.push({
|
||||||
path: newPathSegments.join('.'),
|
path: newPathSegments.join('.'),
|
||||||
type: formatType(definition),
|
type: formatType(definition),
|
||||||
label: definition.label,
|
label: definition.label,
|
||||||
category: definition.category,
|
category: definition.category,
|
||||||
description: formatDescription(definition),
|
description: formatDescription(definition),
|
||||||
defaultValue: formatDefaultValue(definition.default, {
|
defaultValue: formatDefaultValue(defaultValue, {
|
||||||
quoteStrings: true,
|
quoteStrings: true,
|
||||||
}),
|
}),
|
||||||
requiresRestart: Boolean(definition.requiresRestart),
|
requiresRestart: Boolean(definition.requiresRestart),
|
||||||
enumValues: definition.options?.map((option) =>
|
enumValues: definition.options?.map((option) =>
|
||||||
formatDefaultValue(option.value, { quoteStrings: true }),
|
formatDefaultValue(option.value, { quoteStrings: true }),
|
||||||
),
|
),
|
||||||
|
stage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +257,10 @@ function renderSections(sections: Map<string, DocEntry[]>) {
|
|||||||
lines.push(' - **Values:** ' + values);
|
lines.push(' - **Values:** ' + values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.stage) {
|
||||||
|
lines.push(' - **Stage:** ' + entry.stage);
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.requiresRestart) {
|
if (entry.requiresRestart) {
|
||||||
lines.push(' - **Requires restart:** Yes');
|
lines.push(' - **Requires restart:** Yes');
|
||||||
}
|
}
|
||||||
@@ -252,23 +288,34 @@ function renderTableSections(sections: Map<string, DocEntry[]>) {
|
|||||||
}
|
}
|
||||||
lines.push(`### ${title}`);
|
lines.push(`### ${title}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('| UI Label | Setting | Description | Default |');
|
if (section === 'features') {
|
||||||
lines.push('| --- | --- | --- | --- |');
|
lines.push('| UI Label | Setting | Description | Default | Stage |');
|
||||||
|
lines.push('| --- | --- | --- | --- | --- |');
|
||||||
|
} else {
|
||||||
|
lines.push('| UI Label | Setting | Description | Default |');
|
||||||
|
lines.push('| --- | --- | --- | --- |');
|
||||||
|
}
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const val = entry.defaultValue.replace(/\n/g, ' ');
|
const val = entry.defaultValue.replace(/\n/g, ' ');
|
||||||
const defaultVal = '`' + escapeBackticks(val) + '`';
|
const defaultVal = '`' + escapeBackticks(val) + '`';
|
||||||
lines.push(
|
let row =
|
||||||
'| ' +
|
'| ' +
|
||||||
entry.label +
|
entry.label +
|
||||||
' | `' +
|
' | `' +
|
||||||
entry.path +
|
entry.path +
|
||||||
'` | ' +
|
'` | ' +
|
||||||
entry.description +
|
entry.description +
|
||||||
' | ' +
|
' | ' +
|
||||||
defaultVal +
|
defaultVal +
|
||||||
' |',
|
' |';
|
||||||
);
|
|
||||||
|
if (section === 'features') {
|
||||||
|
const stageVal = entry.stage ? '`' + entry.stage + '`' : '-';
|
||||||
|
row += ' ' + stageVal + ' |';
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push('');
|
lines.push('');
|
||||||
@@ -277,6 +324,30 @@ function renderTableSections(sections: Map<string, DocEntry[]>) {
|
|||||||
return lines.join('\n').trimEnd();
|
return lines.join('\n').trimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderFeatureGatesTable() {
|
||||||
|
let markdown = '| Feature | Stage | Default | Since | Description |\n';
|
||||||
|
markdown += '| --- | --- | --- | --- | --- |\n';
|
||||||
|
|
||||||
|
const sortedFeatures = Object.entries(FeatureDefinitions).sort(
|
||||||
|
([keyA], [keyB]) => keyA.localeCompare(keyB),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [key, specs] of sortedFeatures) {
|
||||||
|
const latest = specs[specs.length - 1];
|
||||||
|
const stage = latest.preRelease;
|
||||||
|
const isEnabled =
|
||||||
|
latest.default ??
|
||||||
|
(stage === FeatureStage.Beta || stage === FeatureStage.GA);
|
||||||
|
const defaultValue = isEnabled ? 'Enabled' : 'Disabled';
|
||||||
|
const since = latest.since || '-';
|
||||||
|
const description = latest.description || '-';
|
||||||
|
|
||||||
|
markdown += `| \`${key}\` | ${stage} | ${defaultValue} | ${since} | ${description} |\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.argv[1]) {
|
if (process.argv[1]) {
|
||||||
const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
||||||
if (entryUrl === import.meta.url) {
|
if (entryUrl === import.meta.url) {
|
||||||
|
|||||||
Reference in New Issue
Block a user