mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-24 00:30:59 -07:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd3632c007 | |||
| 4663dff8e2 | |||
| f1bf34ceee | |||
| 0640faa4c6 | |||
| 04f22a51b1 |
@@ -4,6 +4,9 @@
|
||||
"extensionReloading": true,
|
||||
"modelSteering": true
|
||||
},
|
||||
"features": {
|
||||
"allAlpha": true
|
||||
},
|
||||
"general": {
|
||||
"devtools": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
name: 'Feature Lifecycle Tracker'
|
||||
description: 'Track a feature through Alpha, Beta, and GA stages'
|
||||
labels:
|
||||
- 'lifecycle/alpha'
|
||||
- 'status/need-triage'
|
||||
body:
|
||||
- type: 'markdown'
|
||||
attributes:
|
||||
value: |-
|
||||
## 🚀 Feature Overview
|
||||
Provide a concise description of what this feature does and why it is being introduced.
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'spec'
|
||||
attributes:
|
||||
label: 'Feature Specification'
|
||||
description: 'Link to design docs, RFCs, or PRs.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: 'markdown'
|
||||
attributes:
|
||||
value: |-
|
||||
## 🗺️ Progression Roadmap
|
||||
*Maintainers: Update this table and the checkboxes below as the feature matures.*
|
||||
|
||||
| Stage | Targeted Version | Actual Version | Date |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Alpha** | 0.x.x | | |
|
||||
| **Beta** | 0.x.x | | |
|
||||
| **GA** | 0.x.x | | |
|
||||
|
||||
- type: 'checkboxes'
|
||||
id: 'alpha-checklist'
|
||||
attributes:
|
||||
label: 'Alpha Requirements (Disabled by Default)'
|
||||
options:
|
||||
- label: 'Feature gated by Alpha flag in `packages/core/src/config/features.ts`'
|
||||
- label: 'Initial implementation merged'
|
||||
- label: 'Basic unit and integration tests'
|
||||
|
||||
- type: 'checkboxes'
|
||||
id: 'beta-checklist'
|
||||
attributes:
|
||||
label: 'Beta Requirements (Enabled by Default)'
|
||||
options:
|
||||
- label: 'Feature stable (no major architectural changes) for >= 2 minor versions'
|
||||
- label: 'Comprehensive documentation in `docs/`'
|
||||
- label: 'Telemetry/metrics implemented and verified'
|
||||
- label: 'Promotion PR merged (stage moved to Beta)'
|
||||
|
||||
- type: 'checkboxes'
|
||||
id: 'ga-checklist'
|
||||
attributes:
|
||||
label: 'GA Requirements (Locked to Enabled)'
|
||||
options:
|
||||
- label: 'Stable for >= 4 minor versions since Beta'
|
||||
- label: 'No high-priority regressions reported'
|
||||
- label: 'Performance audit complete'
|
||||
- label: 'Migration plan (if applicable) documented'
|
||||
- label: 'PR merged (feature gate removed, code integrated into core)'
|
||||
|
||||
- type: 'textarea'
|
||||
id: 'feedback'
|
||||
attributes:
|
||||
label: 'Key Feedback & Bug Links'
|
||||
description: 'List major bug reports or user feedback threads here.'
|
||||
@@ -0,0 +1,220 @@
|
||||
# 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. **`/features` Command**: Use the `/features` (or `/feature`) command
|
||||
directly in the CLI to list all Alpha, Beta, and Deprecated features. This
|
||||
view shows the maturity stage, enablement status, and metadata like the
|
||||
version it was introduced (`since`) and when it is scheduled for removal
|
||||
(`until`).
|
||||
2. **`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.
|
||||
3. **CLI Flag**: Use `--feature-gates="feat1=true,feat2=false"` for runtime
|
||||
overrides.
|
||||
4. **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.
|
||||
|
||||
## Lifecycle tracking issues
|
||||
|
||||
Every feature managed under this system must have a corresponding **Lifecycle
|
||||
Tracking Issue** on GitHub. These issues act as a living roadmap and a public
|
||||
feedback loop for the feature's progression through Alpha, Beta, and GA stages.
|
||||
|
||||
You can find the link to a feature's tracking issue in the following ways:
|
||||
|
||||
1. **`/features` Command:** The tracker URL is displayed alongside the metadata
|
||||
for each feature.
|
||||
2. **`FeatureDefinitions`:** The `issueUrl` is defined in
|
||||
`packages/core/src/config/features.ts`.
|
||||
|
||||
Maintainers use these issues to document promotion criteria, link related bug
|
||||
reports, and collect user feedback before moving a feature to the next stage.
|
||||
|
||||
### 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. |
|
||||
|
||||
<!-- 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. **Create a Tracker Issue:** Use the **Feature Lifecycle Tracker** template
|
||||
on GitHub to create a new issue. This issue will track the feature from
|
||||
Alpha through GA.
|
||||
2. **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.',
|
||||
issueUrl: 'https://github.com/google-gemini/gemini-cli/issues/123',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
_Note: The `default` field is optional. If omitted, it defaults to `false`
|
||||
for Alpha/Deprecated and `true` for Beta/GA._
|
||||
|
||||
3. **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,
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
4. **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 the Tracker:** Review the requirements in the lifecycle tracker
|
||||
issue. Once met, update the roadmap table in the issue description and post
|
||||
a comment announcing the promotion.
|
||||
2. **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.
|
||||
3. **Update [`settingsSchema.ts`]**: Update the `label` and `description` if
|
||||
necessary.
|
||||
4. **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
|
||||
+16
-12
@@ -137,18 +137,6 @@ they appear in the UI.
|
||||
| --------------------------------- | ------------------------------ | --------------------------------------------- | ------- |
|
||||
| Auto Configure Max Old Space Size | `advanced.autoConfigureMemory` | Automatically configure Node.js memory limits | `false` |
|
||||
|
||||
### Experimental
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| -------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Enable Gemma Model Router | `experimental.gemmaModelRouter.enabled` | Enable the Gemma Model Router. Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
@@ -162,4 +150,20 @@ they appear in the UI.
|
||||
| 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` |
|
||||
|
||||
### 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` |
|
||||
|
||||
<!-- SETTINGS-AUTOGEN:END -->
|
||||
|
||||
+85
-106
@@ -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
|
||||
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 -->
|
||||
|
||||
#### `policyPaths`
|
||||
@@ -953,110 +963,6 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Configuration for the bug report command.
|
||||
- **Default:** `undefined`
|
||||
|
||||
#### `experimental`
|
||||
|
||||
- **`experimental.toolOutputMasking.enabled`** (boolean):
|
||||
- **Description:** Enables tool output masking to save tokens.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.toolOutputMasking.toolProtectionThreshold`** (number):
|
||||
- **Description:** Minimum number of tokens to protect from masking (most
|
||||
recent tool outputs).
|
||||
- **Default:** `50000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.toolOutputMasking.minPrunableTokensThreshold`** (number):
|
||||
- **Description:** Minimum prunable tokens required to trigger a masking pass.
|
||||
- **Default:** `30000`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.toolOutputMasking.protectLatestTurn`** (boolean):
|
||||
- **Description:** Ensures the absolute latest turn is never masked,
|
||||
regardless of token count.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.enableAgents`** (boolean):
|
||||
- **Description:** Enable local and remote subagents. Warning: Experimental
|
||||
feature, uses YOLO mode for subagents
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionManagement`** (boolean):
|
||||
- **Description:** Enable extension management features.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionConfig`** (boolean):
|
||||
- **Description:** Enable requesting and fetching of extension settings.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionRegistry`** (boolean):
|
||||
- **Description:** Enable extension registry explore UI.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionReloading`** (boolean):
|
||||
- **Description:** Enables extension loading/unloading within the CLI session.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.jitContext`** (boolean):
|
||||
- **Description:** Enable Just-In-Time (JIT) context loading.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.useOSC52Paste`** (boolean):
|
||||
- **Description:** Use OSC 52 for pasting. This may be more robust than the
|
||||
default system when using remote terminal sessions (if your terminal is
|
||||
configured to allow it).
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.useOSC52Copy`** (boolean):
|
||||
- **Description:** Use OSC 52 for copying. This may be more robust than the
|
||||
default system when using remote terminal sessions (if your terminal is
|
||||
configured to allow it).
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.plan`** (boolean):
|
||||
- **Description:** Enable planning features (Plan Mode and tools).
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.taskTracker`** (boolean):
|
||||
- **Description:** Enable task tracker tools.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.modelSteering`** (boolean):
|
||||
- **Description:** Enable model steering (user hints) to guide the model
|
||||
during tool execution.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.directWebFetch`** (boolean):
|
||||
- **Description:** Enable web fetch behavior that bypasses LLM summarization.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.enabled`** (boolean):
|
||||
- **Description:** Enable the Gemma Model Router. Requires a local endpoint
|
||||
serving Gemma via the Gemini API using LiteRT-LM shim.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.classifier.host`** (string):
|
||||
- **Description:** The host of the classifier.
|
||||
- **Default:** `"http://localhost:9379"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.classifier.model`** (string):
|
||||
- **Description:** The model to use for the classifier. Only tested on
|
||||
`gemma3-1b-gpu-custom`.
|
||||
- **Default:** `"gemma3-1b-gpu-custom"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `skills`
|
||||
|
||||
- **`skills.enabled`** (boolean):
|
||||
@@ -1165,6 +1071,71 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **`admin.skills.enabled`** (boolean):
|
||||
- **Description:** If false, disallows agent skills from being used.
|
||||
- **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
|
||||
<!-- SETTINGS-AUTOGEN:END -->
|
||||
|
||||
#### `mcpServers`
|
||||
@@ -1350,6 +1321,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
is useful when running Gemini CLI in a standalone terminal while still
|
||||
wanting to associate it with a specific IDE instance.
|
||||
- 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`**:
|
||||
- Specifies the root directory for Gemini CLI's user-level configuration and
|
||||
storage.
|
||||
@@ -1551,13 +1526,17 @@ for that specific session.
|
||||
- `auto_edit`: Automatically approve edit tools (replace, write_file) while
|
||||
prompting for others
|
||||
- `yolo`: Automatically approve all tool calls (equivalent to `--yolo`)
|
||||
- `plan`: Read-only mode for tool calls (requires experimental planning to
|
||||
be enabled).
|
||||
- `plan`: Read-only mode for tool calls (requires planning feature to be
|
||||
enabled).
|
||||
> **Note:** This mode is currently under development and not yet fully
|
||||
> functional.
|
||||
- Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of
|
||||
`--yolo` for the new unified approach.
|
||||
- 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,...>`**:
|
||||
- A comma-separated list of tool names that will bypass the confirmation
|
||||
dialog.
|
||||
|
||||
@@ -161,6 +161,14 @@
|
||||
],
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"packages/cli/src/config/settingsSchema.ts": [
|
||||
"npm run docs:settings",
|
||||
"git add schemas/settings.schema.json docs/reference/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,
|
||||
getExtensionManager,
|
||||
} from './utils.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { loadSettings, isFeatureEnabled } from '../../config/settings.js';
|
||||
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
@@ -45,10 +45,10 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
|
||||
const { name, setting, scope } = args;
|
||||
const settings = loadSettings(process.cwd()).merged;
|
||||
|
||||
if (!(settings.experimental?.extensionConfig ?? true)) {
|
||||
if (!isFeatureEnabled(settings, 'extensionConfig')) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'Extension configuration is currently disabled. Enable it by setting "experimental.extensionConfig" to true.',
|
||||
'Extension configuration is currently disabled. Enable it by setting "features.extensionConfig" to true.',
|
||||
);
|
||||
await exitCli();
|
||||
return;
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
type MergedSettings,
|
||||
saveModelChange,
|
||||
loadSettings,
|
||||
isFeatureEnabled,
|
||||
} from './settings.js';
|
||||
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
@@ -78,6 +79,7 @@ export interface CliArgs {
|
||||
allowedTools: string[] | undefined;
|
||||
acp?: boolean;
|
||||
experimentalAcp?: boolean;
|
||||
featureGates?: string | undefined;
|
||||
extensions: string[] | undefined;
|
||||
listExtensions: boolean | undefined;
|
||||
resume: string | typeof RESUME_LATEST | undefined;
|
||||
@@ -182,6 +184,12 @@ export async function parseArguments(
|
||||
description:
|
||||
'Starts the agent in ACP mode (deprecated, use --acp instead)',
|
||||
})
|
||||
.option('feature-gates', {
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
description:
|
||||
'Comma-separated list of feature key-value pairs (e.g. "Foo=true,Bar=false")',
|
||||
})
|
||||
.option('allowed-mcp-server-names', {
|
||||
type: 'array',
|
||||
string: true,
|
||||
@@ -328,7 +336,7 @@ export async function parseArguments(
|
||||
return true;
|
||||
});
|
||||
|
||||
if (settings.experimental?.extensionManagement) {
|
||||
if (isFeatureEnabled(settings, 'extensionManagement')) {
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
@@ -486,7 +494,7 @@ export async function loadCliConfig(
|
||||
.getExtensions()
|
||||
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
|
||||
|
||||
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
||||
const experimentalJitContext = isFeatureEnabled(settings, 'jitContext');
|
||||
|
||||
let memoryContent: string | HierarchicalMemory = '';
|
||||
let fileCount = 0;
|
||||
@@ -532,7 +540,7 @@ export async function loadCliConfig(
|
||||
approvalMode = ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case 'plan':
|
||||
if (!(settings.experimental?.plan ?? false)) {
|
||||
if (!isFeatureEnabled(settings, 'plan')) {
|
||||
debugLogger.warn(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
|
||||
);
|
||||
@@ -759,15 +767,16 @@ export async function loadCliConfig(
|
||||
bugCommand: settings.advanced?.bugCommand,
|
||||
model: resolvedModel,
|
||||
maxSessionTurns: settings.model?.maxSessionTurns,
|
||||
|
||||
features: settings.features,
|
||||
featureGates: argv.featureGates,
|
||||
listExtensions: argv.listExtensions || false,
|
||||
listSessions: argv.listSessions || false,
|
||||
deleteSession: argv.deleteSession,
|
||||
enabledExtensions: argv.extensions,
|
||||
extensionLoader: extensionManager,
|
||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
plan: settings.experimental?.plan,
|
||||
enableExtensionReloading: isFeatureEnabled(settings, 'extensionReloading'),
|
||||
enableAgents: isFeatureEnabled(settings, 'enableAgents'),
|
||||
plan: isFeatureEnabled(settings, 'plan'),
|
||||
tracker: settings.experimental?.taskTracker,
|
||||
directWebFetch: settings.experimental?.directWebFetch,
|
||||
planSettings: settings.general?.plan?.directory
|
||||
@@ -776,9 +785,14 @@ export async function loadCliConfig(
|
||||
enableEventDrivenScheduler: true,
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
experimentalJitContext: isFeatureEnabled(settings, 'jitContext'),
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
toolOutputMasking: settings.experimental?.toolOutputMasking,
|
||||
toolOutputMasking:
|
||||
(settings.features as Record<string, unknown> | undefined)?.[
|
||||
'toolOutputMasking'
|
||||
] !== undefined
|
||||
? { enabled: isFeatureEnabled(settings, 'toolOutputMasking') }
|
||||
: settings.experimental?.toolOutputMasking,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||
ideMode,
|
||||
|
||||
@@ -9,7 +9,11 @@ import * as path from 'node:path';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import chalk from 'chalk';
|
||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||
import { type MergedSettings, SettingScope } from './settings.js';
|
||||
import {
|
||||
type MergedSettings,
|
||||
SettingScope,
|
||||
isFeatureEnabled,
|
||||
} from './settings.js';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
|
||||
import {
|
||||
@@ -323,7 +327,10 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(destinationPath, { recursive: true });
|
||||
if (this.requestSetting && this.settings.experimental.extensionConfig) {
|
||||
if (
|
||||
this.requestSetting &&
|
||||
isFeatureEnabled(this.settings, 'extensionConfig')
|
||||
) {
|
||||
if (isUpdate) {
|
||||
await maybePromptForSettings(
|
||||
newExtensionConfig,
|
||||
@@ -341,7 +348,10 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
}
|
||||
}
|
||||
|
||||
const missingSettings = this.settings.experimental.extensionConfig
|
||||
const missingSettings = isFeatureEnabled(
|
||||
this.settings,
|
||||
'extensionConfig',
|
||||
)
|
||||
? await getMissingSettings(
|
||||
newExtensionConfig,
|
||||
extensionId,
|
||||
@@ -677,7 +687,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
let userSettings: Record<string, string> = {};
|
||||
let workspaceSettings: Record<string, string> = {};
|
||||
|
||||
if (this.settings.experimental.extensionConfig) {
|
||||
if (isFeatureEnabled(this.settings, 'extensionConfig')) {
|
||||
userSettings = await getScopedEnvContents(
|
||||
config,
|
||||
extensionId,
|
||||
@@ -697,7 +707,10 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
config = resolveEnvVarsInObject(config, customEnv);
|
||||
|
||||
const resolvedSettings: ResolvedExtensionSetting[] = [];
|
||||
if (config.settings && this.settings.experimental.extensionConfig) {
|
||||
if (
|
||||
config.settings &&
|
||||
isFeatureEnabled(this.settings, 'extensionConfig')
|
||||
) {
|
||||
for (const setting of config.settings) {
|
||||
const value = customEnv[setting.envVar];
|
||||
let scope: 'user' | 'workspace' | undefined;
|
||||
|
||||
@@ -206,7 +206,13 @@ describe('extension tests', () => {
|
||||
});
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||
const settings = loadSettings(tempWorkspaceDir).merged;
|
||||
settings.experimental.extensionConfig = true;
|
||||
if (settings.features) {
|
||||
(settings.features as Record<string, unknown>)['extensionConfig'] = true;
|
||||
} else {
|
||||
(settings as unknown as Record<string, unknown>)['features'] = {
|
||||
extensionConfig: true,
|
||||
};
|
||||
}
|
||||
extensionManager = new ExtensionManager({
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: mockRequestConsent,
|
||||
|
||||
@@ -76,6 +76,8 @@ import {
|
||||
LoadedSettings,
|
||||
sanitizeEnvVar,
|
||||
createTestMergedSettings,
|
||||
isFeatureEnabled,
|
||||
type MergedSettings,
|
||||
} from './settings.js';
|
||||
import {
|
||||
FatalConfigError,
|
||||
@@ -3119,4 +3121,50 @@ describe('LoadedSettings Isolation and Serializability', () => {
|
||||
}).toThrow(/Maximum call stack size exceeded/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFeatureEnabled', () => {
|
||||
it('should return true if feature is enabled in "features"', () => {
|
||||
const settings = {
|
||||
features: { plan: true },
|
||||
} as unknown as Settings; // Casting for simplicity
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if feature is disabled in "features"', () => {
|
||||
const settings = {
|
||||
features: { plan: false },
|
||||
} as unknown as Settings;
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should fallback to "experimental" if feature is not in "features"', () => {
|
||||
const settings = {
|
||||
experimental: { plan: true },
|
||||
} as unknown as Settings;
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should prioritize "features" over "experimental"', () => {
|
||||
const settings = {
|
||||
features: { plan: false },
|
||||
experimental: { plan: true },
|
||||
} as unknown as Settings;
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if neither is set', () => {
|
||||
const settings = {} as unknown as Settings;
|
||||
expect(
|
||||
isFeatureEnabled(settings as unknown as MergedSettings, 'plan'),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
coreEvents,
|
||||
homedir,
|
||||
type AdminControlsSettings,
|
||||
DefaultFeatureGate,
|
||||
type FeatureGate,
|
||||
} from '@google/gemini-cli-core';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { DefaultLight } from '../ui/themes/builtin/light/default-light.js';
|
||||
@@ -43,6 +45,57 @@ export {
|
||||
getSettingsSchema,
|
||||
};
|
||||
|
||||
const featureGateCache = new WeakMap<MergedSettings, FeatureGate>();
|
||||
|
||||
/**
|
||||
* Returns true if the feature is enabled in the given settings.
|
||||
* Checks "features" first, then falls back to "experimental".
|
||||
*/
|
||||
export function isFeatureEnabled(
|
||||
settings: MergedSettings,
|
||||
featureName: string,
|
||||
): boolean {
|
||||
let gate = featureGateCache.get(settings);
|
||||
if (!gate) {
|
||||
const mutableGate = DefaultFeatureGate.deepCopy();
|
||||
const overrides: Record<string, boolean> = {};
|
||||
|
||||
// 1. Experimental (Low Priority)
|
||||
const experimental = settings.experimental as Record<string, unknown>;
|
||||
if (experimental) {
|
||||
for (const [key, value] of Object.entries(experimental)) {
|
||||
if (typeof value === 'boolean') {
|
||||
overrides[key] = value;
|
||||
}
|
||||
}
|
||||
// Handle nested toolOutputMasking legacy structure
|
||||
const toolOutputMasking = experimental['toolOutputMasking'];
|
||||
if (
|
||||
toolOutputMasking &&
|
||||
typeof toolOutputMasking === 'object' &&
|
||||
'enabled' in toolOutputMasking &&
|
||||
typeof (toolOutputMasking as Record<string, unknown>)['enabled'] ===
|
||||
'boolean'
|
||||
) {
|
||||
overrides['toolOutputMasking'] = Boolean(
|
||||
(toolOutputMasking as Record<string, unknown>)['enabled'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Features (High Priority)
|
||||
if (settings.features) {
|
||||
Object.assign(overrides, settings.features);
|
||||
}
|
||||
|
||||
mutableGate.setFromMap(overrides);
|
||||
gate = mutableGate;
|
||||
featureGateCache.set(settings, gate);
|
||||
}
|
||||
|
||||
return gate.enabled(featureName);
|
||||
}
|
||||
|
||||
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
|
||||
import { customDeepMerge } from '../utils/deepMerge.js';
|
||||
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
|
||||
|
||||
@@ -1687,7 +1687,9 @@ const SETTINGS_SCHEMA = {
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Setting to enable experimental features',
|
||||
ignoreInDocs: true,
|
||||
description:
|
||||
'DEPRECATED: Use the "features" object instead. Setting to enable experimental features',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
toolOutputMasking: {
|
||||
@@ -1708,7 +1710,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enables tool output masking to save tokens.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
toolProtectionThreshold: {
|
||||
type: 'number',
|
||||
@@ -1825,7 +1827,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable planning features (Plan Mode and tools).',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
taskTracker: {
|
||||
type: 'boolean',
|
||||
@@ -2277,6 +2279,118 @@ 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const satisfies SettingsSchema;
|
||||
|
||||
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
|
||||
|
||||
@@ -31,6 +31,7 @@ import { docsCommand } from '../ui/commands/docsCommand.js';
|
||||
import { directoryCommand } from '../ui/commands/directoryCommand.js';
|
||||
import { editorCommand } from '../ui/commands/editorCommand.js';
|
||||
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
|
||||
import { featuresCommand } from '../ui/commands/featuresCommand.js';
|
||||
import { footerCommand } from '../ui/commands/footerCommand.js';
|
||||
import { helpCommand } from '../ui/commands/helpCommand.js';
|
||||
import { shortcutsCommand } from '../ui/commands/shortcutsCommand.js';
|
||||
@@ -119,6 +120,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
},
|
||||
]
|
||||
: [extensionsCommand(this.config?.getEnableExtensionReloading())]),
|
||||
featuresCommand,
|
||||
helpCommand,
|
||||
footerCommand,
|
||||
shortcutsCommand,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, type vi } from 'vitest';
|
||||
import { featuresCommand } from './featuresCommand.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import { MessageType, type HistoryItemFeaturesList } from '../types.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
|
||||
describe('featuresCommand', () => {
|
||||
it('should display an error if the feature gate is unavailable', async () => {
|
||||
const mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getFeatureGate: () => undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!featuresCommand.action) throw new Error('Action not defined');
|
||||
await featuresCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Could not retrieve feature gate.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should list alpha, beta, and deprecated features', async () => {
|
||||
const mockFeatures = [
|
||||
{ key: 'alphaFeat', enabled: false, stage: FeatureStage.Alpha },
|
||||
{ key: 'betaFeat', enabled: true, stage: FeatureStage.Beta },
|
||||
{ key: 'deprecatedFeat', enabled: false, stage: FeatureStage.Deprecated },
|
||||
{ key: 'gaFeat', enabled: true, stage: FeatureStage.GA },
|
||||
];
|
||||
|
||||
const mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getFeatureGate: () => ({
|
||||
getFeatureInfo: () => mockFeatures,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!featuresCommand.action) throw new Error('Action not defined');
|
||||
await featuresCommand.action(mockContext, '');
|
||||
|
||||
const [message] = (mockContext.ui.addItem as ReturnType<typeof vi.fn>).mock
|
||||
.calls[0];
|
||||
const featuresList = message as HistoryItemFeaturesList;
|
||||
expect(featuresList.type).toBe(MessageType.FEATURES_LIST);
|
||||
expect(featuresList.features).toHaveLength(3);
|
||||
expect(featuresList.features.map((f) => f.key)).toEqual([
|
||||
'alphaFeat',
|
||||
'betaFeat',
|
||||
'deprecatedFeat',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
import { MessageType, type HistoryItemFeaturesList } from '../types.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
|
||||
export const featuresCommand: SlashCommand = {
|
||||
name: 'features',
|
||||
altNames: ['feature'],
|
||||
description: 'List alpha, beta, and deprecated Gemini CLI features.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context: CommandContext): Promise<void> => {
|
||||
const featureGate = context.services.config?.getFeatureGate();
|
||||
if (!featureGate) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Could not retrieve feature gate.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const allFeatures = featureGate.getFeatureInfo();
|
||||
const filteredFeatures = allFeatures.filter(
|
||||
(f) =>
|
||||
f.stage === FeatureStage.Alpha ||
|
||||
f.stage === FeatureStage.Beta ||
|
||||
f.stage === FeatureStage.Deprecated,
|
||||
);
|
||||
|
||||
const featuresListItem: HistoryItemFeaturesList = {
|
||||
type: MessageType.FEATURES_LIST,
|
||||
features: filteredFeatures,
|
||||
};
|
||||
|
||||
context.ui.addItem(featuresListItem);
|
||||
},
|
||||
};
|
||||
@@ -29,6 +29,7 @@ import { ExtensionsList } from './views/ExtensionsList.js';
|
||||
import { getMCPServerStatus } from '@google/gemini-cli-core';
|
||||
import { ToolsList } from './views/ToolsList.js';
|
||||
import { SkillsList } from './views/SkillsList.js';
|
||||
import { FeaturesList } from './views/FeaturesList.js';
|
||||
import { AgentsStatus } from './views/AgentsStatus.js';
|
||||
import { McpStatus } from './views/McpStatus.js';
|
||||
import { ChatList } from './views/ChatList.js';
|
||||
@@ -205,6 +206,9 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
showDescriptions={itemForDisplay.showDescriptions}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'features_list' && (
|
||||
<FeaturesList features={itemForDisplay.features} />
|
||||
)}
|
||||
{itemForDisplay.type === 'agents_list' && (
|
||||
<AgentsStatus
|
||||
agents={itemForDisplay.agents}
|
||||
|
||||
@@ -415,7 +415,7 @@ describe('SettingsDialog', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
// Should wrap to last setting (without relying on exact bullet character)
|
||||
expect(lastFrame()).toContain('Hook Notifications');
|
||||
expect(lastFrame()).toContain('Zed Integration');
|
||||
});
|
||||
|
||||
unmount();
|
||||
@@ -753,6 +753,33 @@ describe('SettingsDialog', () => {
|
||||
});
|
||||
|
||||
describe('Specific Settings Behavior', () => {
|
||||
it('should render feature stage badges', async () => {
|
||||
const featureSettings = createMockSettings({
|
||||
'features.allAlpha': false,
|
||||
'features.allBeta': true,
|
||||
});
|
||||
|
||||
const onSelect = vi.fn();
|
||||
const { lastFrame, stdin } = render(
|
||||
<KeypressProvider>
|
||||
<SettingsDialog
|
||||
settings={featureSettings}
|
||||
onSelect={onSelect}
|
||||
availableTerminalHeight={100}
|
||||
/>
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
stdin.write('Plan');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Plan Mode');
|
||||
expect(lastFrame()).toContain('[ALPHA]');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show correct display values for settings with different states', async () => {
|
||||
const settings = createMockSettings({
|
||||
user: {
|
||||
|
||||
@@ -33,7 +33,12 @@ import {
|
||||
type SettingsValue,
|
||||
TOGGLE_TYPES,
|
||||
} from '../../config/settingsSchema.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import {
|
||||
coreEvents,
|
||||
debugLogger,
|
||||
FeatureDefinitions,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
import { useSearchBuffer } from '../hooks/useSearchBuffer.js';
|
||||
import {
|
||||
@@ -244,6 +249,14 @@ export function SettingsDialog({
|
||||
// The inline editor needs a string but non primitive settings like Arrays and Objects exist
|
||||
const editValue = getEditValue(type, rawValue);
|
||||
|
||||
let stage: string | undefined;
|
||||
const definitionKey = key.replace(/^features\./, '');
|
||||
if (key.startsWith('features.') && FeatureDefinitions[definitionKey]) {
|
||||
const specs = FeatureDefinitions[definitionKey];
|
||||
const latest = specs[specs.length - 1];
|
||||
stage = latest.preRelease;
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
label: definition?.label || key,
|
||||
@@ -252,8 +265,10 @@ export function SettingsDialog({
|
||||
displayValue,
|
||||
isGreyedOut,
|
||||
scopeMessage,
|
||||
rawValue,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
rawValue: rawValue as string | number | boolean | undefined,
|
||||
editValue,
|
||||
stage,
|
||||
};
|
||||
});
|
||||
}, [settingKeys, selectedScope, settings]);
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
import { formatCommand } from '../../utils/keybindingUtils.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
|
||||
/**
|
||||
* Represents a single item in the settings dialog.
|
||||
@@ -49,6 +50,8 @@ export interface SettingsDialogItem {
|
||||
rawValue?: SettingsValue;
|
||||
/** Optional pre-formatted edit buffer value for complex types */
|
||||
editValue?: string;
|
||||
/** Feature stage (e.g. ALPHA, BETA) */
|
||||
stage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -553,6 +556,20 @@ export function BaseSettingsDialog({
|
||||
color={isActive ? theme.ui.focus : theme.text.primary}
|
||||
>
|
||||
{item.label}
|
||||
{item.stage && item.stage !== FeatureStage.GA && (
|
||||
<Text
|
||||
color={
|
||||
item.stage === FeatureStage.Deprecated
|
||||
? theme.status.error
|
||||
: item.stage === FeatureStage.Beta
|
||||
? theme.text.accent
|
||||
: theme.status.warning
|
||||
}
|
||||
>
|
||||
{' '}
|
||||
[{item.stage}]{' '}
|
||||
</Text>
|
||||
)}
|
||||
{item.scopeMessage && (
|
||||
<Text color={theme.text.secondary}>
|
||||
{' '}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { FeaturesList } from './FeaturesList.js';
|
||||
import { type FeatureInfo } from '../../types.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
|
||||
const mockFeatures: FeatureInfo[] = [
|
||||
{
|
||||
key: 'alphaFeat',
|
||||
enabled: false,
|
||||
stage: FeatureStage.Alpha,
|
||||
since: '0.30.0',
|
||||
description: 'An alpha feature.',
|
||||
},
|
||||
{
|
||||
key: 'betaFeat',
|
||||
enabled: true,
|
||||
stage: FeatureStage.Beta,
|
||||
since: '0.29.0',
|
||||
description: 'A beta feature.',
|
||||
},
|
||||
{
|
||||
key: 'deprecatedFeat',
|
||||
enabled: false,
|
||||
stage: FeatureStage.Deprecated,
|
||||
since: '0.28.0',
|
||||
until: '0.31.0',
|
||||
description: 'A deprecated feature.',
|
||||
},
|
||||
];
|
||||
|
||||
describe('<FeaturesList />', () => {
|
||||
it('renders correctly with features', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<FeaturesList features={mockFeatures} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with no features', () => {
|
||||
const { lastFrame } = renderWithProviders(<FeaturesList features={[]} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { type FeatureInfo } from '../../types.js';
|
||||
import { FeatureStage } from '@google/gemini-cli-core';
|
||||
|
||||
interface FeaturesListProps {
|
||||
features: FeatureInfo[];
|
||||
}
|
||||
|
||||
export const FeaturesList: React.FC<FeaturesListProps> = ({ features }) => {
|
||||
if (features.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={theme.text.primary}> No features found</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const alphaFeatures = features.filter((f) => f.stage === FeatureStage.Alpha);
|
||||
const betaFeatures = features.filter((f) => f.stage === FeatureStage.Beta);
|
||||
const deprecatedFeatures = features.filter(
|
||||
(f) => f.stage === FeatureStage.Deprecated,
|
||||
);
|
||||
|
||||
// Column widths as percentages/proportions
|
||||
const colWidths = {
|
||||
feature: '40%',
|
||||
status: '20%',
|
||||
since: '20%',
|
||||
until: '20%',
|
||||
};
|
||||
|
||||
const renderSection = (
|
||||
title: string,
|
||||
sectionFeatures: FeatureInfo[],
|
||||
stageColor: string,
|
||||
) => {
|
||||
if (sectionFeatures.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text bold color={stageColor}>
|
||||
┃
|
||||
</Text>
|
||||
<Box marginLeft={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{' '}
|
||||
({sectionFeatures.length})
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Table Header */}
|
||||
<Box
|
||||
flexDirection="row"
|
||||
paddingX={1}
|
||||
borderStyle="single"
|
||||
borderTop={true}
|
||||
borderBottom={true}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
<Box width={colWidths.feature}>
|
||||
<Text bold color={theme.text.secondary}>
|
||||
FEATURE
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.status}>
|
||||
<Text bold color={theme.text.secondary}>
|
||||
STATUS
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.since}>
|
||||
<Text bold color={theme.text.secondary}>
|
||||
SINCE
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.until}>
|
||||
<Text bold color={theme.text.secondary}>
|
||||
UNTIL
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Table Rows */}
|
||||
{sectionFeatures.map((feature) => (
|
||||
<Box
|
||||
key={feature.key}
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingTop={1}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
<Box width={colWidths.feature}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{feature.key}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.status}>
|
||||
<Box flexDirection="row">
|
||||
<Text>{feature.enabled ? '🟢 ' : '🔴 '}</Text>
|
||||
<Text
|
||||
color={
|
||||
feature.enabled
|
||||
? theme.status.success
|
||||
: theme.status.error
|
||||
}
|
||||
>
|
||||
{feature.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box width={colWidths.since}>
|
||||
<Text color={theme.text.secondary}>{feature.since || '—'}</Text>
|
||||
</Box>
|
||||
<Box width={colWidths.until}>
|
||||
<Text color={theme.text.secondary}>{feature.until || '—'}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{feature.description && (
|
||||
<Box marginLeft={2}>
|
||||
<Text dimColor color={theme.text.primary}>
|
||||
{feature.description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{feature.issueUrl && (
|
||||
<Box marginLeft={2} marginBottom={1}>
|
||||
<Text color={theme.text.accent}>
|
||||
Tracker: <Text dimColor>{feature.issueUrl}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{!feature.issueUrl && feature.description && (
|
||||
<Box marginBottom={1} />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{renderSection('Alpha Features', alphaFeatures, theme.status.error)}
|
||||
{renderSection('Beta Features', betaFeatures, theme.status.warning)}
|
||||
{renderSection(
|
||||
'Deprecated Features',
|
||||
deprecatedFeatures,
|
||||
theme.text.secondary,
|
||||
)}
|
||||
|
||||
<Box flexDirection="row" marginTop={1} paddingX={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
💡 Use{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
/settings
|
||||
</Text>{' '}
|
||||
to enable or disable features. You can also use stage toggles like{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
allAlpha=true
|
||||
</Text>{' '}
|
||||
or{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
allBeta=false
|
||||
</Text>{' '}
|
||||
to toggle entire stages.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<FeaturesList /> > renders correctly with features 1`] = `
|
||||
"┃ Alpha Features (1)
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
FEATURE STATUS SINCE UNTIL
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
alphaFeat 🔴 Disabled 0.30.0 —
|
||||
An alpha feature.
|
||||
|
||||
|
||||
┃ Beta Features (1)
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
FEATURE STATUS SINCE UNTIL
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
betaFeat 🟢 Enabled 0.29.0 —
|
||||
A beta feature.
|
||||
|
||||
|
||||
┃ Deprecated Features (1)
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
FEATURE STATUS SINCE UNTIL
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
deprecatedFeat 🔴 Disabled 0.28.0 0.31.0
|
||||
A deprecated feature.
|
||||
|
||||
|
||||
|
||||
💡 Use /settings to enable or disable features. You can also use stage toggles like allAlpha=true or allBeta=false to
|
||||
toggle entire stages.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<FeaturesList /> > renders correctly with no features 1`] = `
|
||||
" No features found
|
||||
"
|
||||
`;
|
||||
@@ -15,13 +15,14 @@ import {
|
||||
type SkillDefinition,
|
||||
type AgentDefinition,
|
||||
type ApprovalMode,
|
||||
type FeatureInfo,
|
||||
CoreToolCallStatus,
|
||||
checkExhaustive,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { PartListUnion } from '@google/genai';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
export type { ThoughtSummary, SkillDefinition };
|
||||
export type { ThoughtSummary, SkillDefinition, FeatureInfo };
|
||||
|
||||
export enum AuthState {
|
||||
// Attempting to authenticate or re-authenticate
|
||||
@@ -288,6 +289,11 @@ export type HistoryItemSkillsList = HistoryItemBase & {
|
||||
showDescriptions: boolean;
|
||||
};
|
||||
|
||||
export type HistoryItemFeaturesList = HistoryItemBase & {
|
||||
type: 'features_list';
|
||||
features: FeatureInfo[];
|
||||
};
|
||||
|
||||
export type AgentDefinitionJson = Pick<
|
||||
AgentDefinition,
|
||||
'name' | 'displayName' | 'description' | 'kind'
|
||||
@@ -374,6 +380,7 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemExtensionsList
|
||||
| HistoryItemToolsList
|
||||
| HistoryItemSkillsList
|
||||
| HistoryItemFeaturesList
|
||||
| HistoryItemAgentsList
|
||||
| HistoryItemMcpStatus
|
||||
| HistoryItemChatList
|
||||
@@ -399,6 +406,7 @@ export enum MessageType {
|
||||
EXTENSIONS_LIST = 'extensions_list',
|
||||
TOOLS_LIST = 'tools_list',
|
||||
SKILLS_LIST = 'skills_list',
|
||||
FEATURES_LIST = 'features_list',
|
||||
AGENTS_LIST = 'agents_list',
|
||||
MCP_STATUS = 'mcp_status',
|
||||
CHAT_LIST = 'chat_list',
|
||||
|
||||
@@ -2391,7 +2391,100 @@ 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,
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshAuth', () => {
|
||||
const baseModel = 'test-model';
|
||||
const baseParams: ConfigParameters = {
|
||||
sessionId: 'test',
|
||||
|
||||
@@ -114,6 +114,7 @@ import { WorkspaceContext } from '../utils/workspaceContext.js';
|
||||
import { Storage } from './storage.js';
|
||||
import type { ShellExecutionConfig } from '../services/shellExecutionService.js';
|
||||
import { FileExclusions } from '../utils/ignorePatterns.js';
|
||||
import { DefaultFeatureGate, type FeatureGate } from './features.js';
|
||||
import { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import type { EventEmitter } from 'node:events';
|
||||
import { PolicyEngine } from '../policy/policy-engine.js';
|
||||
@@ -584,6 +585,8 @@ export interface ConfigParameters {
|
||||
tracker?: boolean;
|
||||
planSettings?: PlanSettings;
|
||||
modelSteering?: boolean;
|
||||
features?: Record<string, boolean>;
|
||||
featureGates?: string;
|
||||
onModelChange?: (model: string) => void;
|
||||
mcpEnabled?: boolean;
|
||||
extensionsEnabled?: boolean;
|
||||
@@ -730,7 +733,6 @@ export class Config implements McpContext {
|
||||
private readonly useBackgroundColor: boolean;
|
||||
private readonly useAlternateBuffer: boolean;
|
||||
private shellExecutionConfig: ShellExecutionConfig;
|
||||
private readonly extensionManagement: boolean = true;
|
||||
private readonly truncateToolOutputThreshold: number;
|
||||
private compressionTruncationCounter = 0;
|
||||
private initialized = false;
|
||||
@@ -784,14 +786,14 @@ export class Config implements McpContext {
|
||||
overageStrategy: OverageStrategy;
|
||||
};
|
||||
|
||||
private readonly enableAgents: boolean;
|
||||
private agents: AgentSettings;
|
||||
private readonly enableEventDrivenScheduler: boolean;
|
||||
private readonly skillsSupport: boolean;
|
||||
private disabledSkills: string[];
|
||||
private readonly adminSkillsEnabled: boolean;
|
||||
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly featureGate: FeatureGate;
|
||||
|
||||
private readonly disableLLMCorrection: boolean;
|
||||
private readonly planEnabled: boolean;
|
||||
private readonly trackerEnabled: boolean;
|
||||
@@ -881,7 +883,6 @@ export class Config implements McpContext {
|
||||
this.model = params.model;
|
||||
this.disableLoopDetection = params.disableLoopDetection ?? false;
|
||||
this._activeModel = params.model;
|
||||
this.enableAgents = params.enableAgents ?? false;
|
||||
this.agents = params.agents ?? {};
|
||||
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
|
||||
this.planEnabled = params.plan ?? false;
|
||||
@@ -891,8 +892,41 @@ export class Config implements McpContext {
|
||||
this.skillsSupport = params.skillsSupport ?? true;
|
||||
this.disabledSkills = params.disabledSkills ?? [];
|
||||
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,
|
||||
};
|
||||
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.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||
this.modelSteering = params.modelSteering ?? false;
|
||||
this.userHintService = new UserHintService(() =>
|
||||
this.isModelSteeringEnabled(),
|
||||
@@ -959,7 +993,6 @@ export class Config implements McpContext {
|
||||
params.enableShellOutputEfficiency ?? true;
|
||||
this.shellToolInactivityTimeout =
|
||||
(params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes
|
||||
this.extensionManagement = params.extensionManagement ?? true;
|
||||
this.enableExtensionReloading = params.enableExtensionReloading ?? false;
|
||||
this.storage = new Storage(this.targetDir, this.sessionId);
|
||||
this.storage.setCustomPlansDir(params.planSettings?.directory);
|
||||
@@ -1088,10 +1121,24 @@ export class Config implements McpContext {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the feature gate for querying feature status.
|
||||
*/
|
||||
getFeatureGate(): FeatureGate {
|
||||
return this.featureGate;
|
||||
}
|
||||
|
||||
isInitialized(): boolean {
|
||||
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
|
||||
* once.
|
||||
@@ -1193,7 +1240,7 @@ export class Config implements McpContext {
|
||||
await this.hookSystem.initialize();
|
||||
}
|
||||
|
||||
if (this.experimentalJitContext) {
|
||||
if (this.isFeatureEnabled('jitContext')) {
|
||||
this.contextManager = new ContextManager(this);
|
||||
await this.contextManager.refresh();
|
||||
}
|
||||
@@ -1853,7 +1900,7 @@ export class Config implements McpContext {
|
||||
}
|
||||
|
||||
getUserMemory(): string | HierarchicalMemory {
|
||||
if (this.experimentalJitContext && this.contextManager) {
|
||||
if (this.isFeatureEnabled('jitContext') && this.contextManager) {
|
||||
return {
|
||||
global: this.contextManager.getGlobalMemory(),
|
||||
extension: this.contextManager.getExtensionMemory(),
|
||||
@@ -1867,7 +1914,7 @@ export class Config implements McpContext {
|
||||
* Refreshes the MCP context, including memory, tools, and system instructions.
|
||||
*/
|
||||
async refreshMcpContext(): Promise<void> {
|
||||
if (this.experimentalJitContext && this.contextManager) {
|
||||
if (this.isFeatureEnabled('jitContext') && this.contextManager) {
|
||||
await this.contextManager.refresh();
|
||||
} else {
|
||||
const { refreshServerHierarchicalMemory } = await import(
|
||||
@@ -1898,7 +1945,7 @@ export class Config implements McpContext {
|
||||
}
|
||||
|
||||
isJitContextEnabled(): boolean {
|
||||
return this.experimentalJitContext;
|
||||
return this.isFeatureEnabled('jitContext');
|
||||
}
|
||||
|
||||
isModelSteeringEnabled(): boolean {
|
||||
@@ -1906,7 +1953,7 @@ export class Config implements McpContext {
|
||||
}
|
||||
|
||||
getToolOutputMaskingEnabled(): boolean {
|
||||
return this.toolOutputMasking.enabled;
|
||||
return this.isFeatureEnabled('toolOutputMasking');
|
||||
}
|
||||
|
||||
async getToolOutputMaskingConfig(): Promise<ToolOutputMaskingConfig> {
|
||||
@@ -1930,7 +1977,7 @@ export class Config implements McpContext {
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
enabled: this.toolOutputMasking.enabled,
|
||||
enabled: this.getToolOutputMaskingEnabled(),
|
||||
toolProtectionThreshold:
|
||||
parsedProtection !== undefined && !isNaN(parsedProtection)
|
||||
? parsedProtection
|
||||
@@ -1945,7 +1992,7 @@ export class Config implements McpContext {
|
||||
}
|
||||
|
||||
getGeminiMdFileCount(): number {
|
||||
if (this.experimentalJitContext && this.contextManager) {
|
||||
if (this.isFeatureEnabled('jitContext') && this.contextManager) {
|
||||
return this.contextManager.getLoadedPaths().size;
|
||||
}
|
||||
return this.geminiMdFileCount;
|
||||
@@ -1956,7 +2003,7 @@ export class Config implements McpContext {
|
||||
}
|
||||
|
||||
getGeminiMdFilePaths(): string[] {
|
||||
if (this.experimentalJitContext && this.contextManager) {
|
||||
if (this.isFeatureEnabled('jitContext') && this.contextManager) {
|
||||
return Array.from(this.contextManager.getLoadedPaths());
|
||||
}
|
||||
return this.geminiMdFilePaths;
|
||||
@@ -2259,7 +2306,7 @@ export class Config implements McpContext {
|
||||
}
|
||||
|
||||
getExtensionManagement(): boolean {
|
||||
return this.extensionManagement;
|
||||
return this.isFeatureEnabled('extensionManagement');
|
||||
}
|
||||
|
||||
getExtensions(): GeminiCLIExtension[] {
|
||||
@@ -2285,7 +2332,7 @@ export class Config implements McpContext {
|
||||
}
|
||||
|
||||
isPlanEnabled(): boolean {
|
||||
return this.planEnabled;
|
||||
return this.isFeatureEnabled('plan');
|
||||
}
|
||||
|
||||
isTrackerEnabled(): boolean {
|
||||
@@ -2305,7 +2352,7 @@ export class Config implements McpContext {
|
||||
}
|
||||
|
||||
isAgentsEnabled(): boolean {
|
||||
return this.enableAgents;
|
||||
return this.isFeatureEnabled('enableAgents');
|
||||
}
|
||||
|
||||
isEventDrivenSchedulerEnabled(): boolean {
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* @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 return feature info with metadata', () => {
|
||||
const gate = DefaultFeatureGate.deepCopy();
|
||||
gate.add({
|
||||
feat1: [
|
||||
{
|
||||
preRelease: FeatureStage.Alpha,
|
||||
since: '0.1.0',
|
||||
description: 'Feature 1',
|
||||
},
|
||||
],
|
||||
feat2: [
|
||||
{
|
||||
preRelease: FeatureStage.Beta,
|
||||
since: '0.2.0',
|
||||
until: '0.3.0',
|
||||
description: 'Feature 2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const info = gate.getFeatureInfo();
|
||||
const feat1 = info.find((f) => f.key === 'feat1');
|
||||
const feat2 = info.find((f) => f.key === 'feat2');
|
||||
|
||||
expect(feat1).toEqual({
|
||||
key: 'feat1',
|
||||
enabled: false,
|
||||
stage: FeatureStage.Alpha,
|
||||
since: '0.1.0',
|
||||
until: undefined,
|
||||
description: 'Feature 1',
|
||||
issueUrl: undefined,
|
||||
});
|
||||
expect(feat2).toEqual({
|
||||
key: 'feat2',
|
||||
enabled: true,
|
||||
stage: FeatureStage.Beta,
|
||||
since: '0.2.0',
|
||||
until: '0.3.0',
|
||||
description: 'Feature 2',
|
||||
issueUrl: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should include issueUrl in feature info', () => {
|
||||
const gate = DefaultFeatureGate.deepCopy();
|
||||
gate.add({
|
||||
featWithUrl: [
|
||||
{
|
||||
preRelease: FeatureStage.Alpha,
|
||||
issueUrl: 'https://github.com/google/gemini-cli/issues/1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const info = gate.getFeatureInfo();
|
||||
const feat = info.find((f) => f.key === 'featWithUrl');
|
||||
|
||||
expect(feat).toMatchObject({
|
||||
key: 'featWithUrl',
|
||||
issueUrl: 'https://github.com/google/gemini-cli/issues/1',
|
||||
});
|
||||
});
|
||||
|
||||
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,317 @@
|
||||
/**
|
||||
* @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;
|
||||
/**
|
||||
* Link to the Lifecycle Tracking Issue on GitHub.
|
||||
*/
|
||||
issueUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FeatureInfo provides a summary of a feature's current state and metadata.
|
||||
*/
|
||||
export interface FeatureInfo {
|
||||
key: string;
|
||||
enabled: boolean;
|
||||
stage: FeatureStage;
|
||||
since?: string;
|
||||
until?: string;
|
||||
description?: string;
|
||||
issueUrl?: 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 all features with their status and metadata.
|
||||
*/
|
||||
getFeatureInfo(): FeatureInfo[];
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
getFeatureInfo(): FeatureInfo[] {
|
||||
return Array.from(this.specs.entries())
|
||||
.map(([key, specs]) => {
|
||||
const latestSpec = specs[specs.length - 1];
|
||||
return {
|
||||
key,
|
||||
enabled: this.enabled(key),
|
||||
stage: latestSpec.preRelease,
|
||||
since: latestSpec.since,
|
||||
until: latestSpec.until,
|
||||
description: latestSpec.description,
|
||||
issueUrl: latestSpec.issueUrl,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.key.localeCompare(b.key));
|
||||
}
|
||||
|
||||
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).',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Register core features
|
||||
DefaultFeatureGate.add(FeatureDefinitions);
|
||||
@@ -7,6 +7,7 @@
|
||||
// Export config
|
||||
export * from './config/config.js';
|
||||
export * from './config/memory.js';
|
||||
export * from './config/features.js';
|
||||
export * from './config/defaultModelConfigs.js';
|
||||
export * from './config/models.js';
|
||||
export * from './config/constants.js';
|
||||
|
||||
@@ -1611,8 +1611,8 @@
|
||||
},
|
||||
"experimental": {
|
||||
"title": "Experimental",
|
||||
"description": "Setting to enable experimental features",
|
||||
"markdownDescription": "Setting to enable experimental features\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `{}`",
|
||||
"description": "DEPRECATED: Use the \"features\" object instead. Setting to enable experimental features",
|
||||
"markdownDescription": "DEPRECATED: Use the \"features\" object instead. Setting to enable experimental features\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `{}`",
|
||||
"default": {},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2040,6 +2040,93 @@
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
|
||||
@@ -22,6 +22,11 @@ import type {
|
||||
SettingsSchemaType,
|
||||
} from '../packages/cli/src/config/settingsSchema.js';
|
||||
|
||||
import {
|
||||
FeatureDefinitions,
|
||||
FeatureStage,
|
||||
} from '../packages/core/src/config/features.js';
|
||||
|
||||
const START_MARKER = '<!-- SETTINGS-AUTOGEN:START -->';
|
||||
const END_MARKER = '<!-- SETTINGS-AUTOGEN:END -->';
|
||||
|
||||
@@ -36,6 +41,7 @@ interface DocEntry {
|
||||
defaultValue: string;
|
||||
requiresRestart: boolean;
|
||||
enumValues?: string[];
|
||||
stage?: string;
|
||||
}
|
||||
|
||||
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 cliSettingsDocPath = path.join(repoRoot, 'docs/cli/settings.md');
|
||||
const featureGatesDocPath = path.join(
|
||||
repoRoot,
|
||||
'docs/cli/feature-lifecycle.md',
|
||||
);
|
||||
|
||||
const { getSettingsSchema } = await loadSettingsSchemaModule();
|
||||
const schema = getSettingsSchema();
|
||||
@@ -59,21 +69,31 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
|
||||
const generatedBlock = renderSections(allSettingsSections);
|
||||
const generatedTableBlock = renderTableSections(filteredSettingsSections);
|
||||
const generatedFeatureGatesBlock = renderFeatureGatesTable();
|
||||
|
||||
await updateFile(docPath, generatedBlock, checkOnly);
|
||||
await updateFile(cliSettingsDocPath, generatedTableBlock, checkOnly);
|
||||
await updateFile(
|
||||
featureGatesDocPath,
|
||||
generatedFeatureGatesBlock,
|
||||
checkOnly,
|
||||
'<!-- FEATURES-AUTOGEN:START -->',
|
||||
'<!-- FEATURES-AUTOGEN:END -->',
|
||||
);
|
||||
}
|
||||
|
||||
async function updateFile(
|
||||
filePath: string,
|
||||
newContent: string,
|
||||
checkOnly: boolean,
|
||||
startMarker = START_MARKER,
|
||||
endMarker = END_MARKER,
|
||||
) {
|
||||
const doc = await readFile(filePath, 'utf8');
|
||||
const injectedDoc = injectBetweenMarkers({
|
||||
document: doc,
|
||||
startMarker: START_MARKER,
|
||||
endMarker: END_MARKER,
|
||||
startMarker,
|
||||
endMarker,
|
||||
newContent: newContent,
|
||||
paddingBefore: '\n',
|
||||
paddingAfter: '\n',
|
||||
@@ -142,19 +162,31 @@ function collectEntries(
|
||||
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({
|
||||
path: newPathSegments.join('.'),
|
||||
type: formatType(definition),
|
||||
label: definition.label,
|
||||
category: definition.category,
|
||||
description: formatDescription(definition),
|
||||
defaultValue: formatDefaultValue(definition.default, {
|
||||
defaultValue: formatDefaultValue(defaultValue, {
|
||||
quoteStrings: true,
|
||||
}),
|
||||
requiresRestart: Boolean(definition.requiresRestart),
|
||||
enumValues: definition.options?.map((option) =>
|
||||
formatDefaultValue(option.value, { quoteStrings: true }),
|
||||
),
|
||||
stage,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,6 +257,10 @@ function renderSections(sections: Map<string, DocEntry[]>) {
|
||||
lines.push(' - **Values:** ' + values);
|
||||
}
|
||||
|
||||
if (entry.stage) {
|
||||
lines.push(' - **Stage:** ' + entry.stage);
|
||||
}
|
||||
|
||||
if (entry.requiresRestart) {
|
||||
lines.push(' - **Requires restart:** Yes');
|
||||
}
|
||||
@@ -252,23 +288,34 @@ function renderTableSections(sections: Map<string, DocEntry[]>) {
|
||||
}
|
||||
lines.push(`### ${title}`);
|
||||
lines.push('');
|
||||
lines.push('| UI Label | Setting | Description | Default |');
|
||||
lines.push('| --- | --- | --- | --- |');
|
||||
if (section === 'features') {
|
||||
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) {
|
||||
const val = entry.defaultValue.replace(/\n/g, ' ');
|
||||
const defaultVal = '`' + escapeBackticks(val) + '`';
|
||||
lines.push(
|
||||
let row =
|
||||
'| ' +
|
||||
entry.label +
|
||||
' | `' +
|
||||
entry.path +
|
||||
'` | ' +
|
||||
entry.description +
|
||||
' | ' +
|
||||
defaultVal +
|
||||
' |',
|
||||
);
|
||||
entry.label +
|
||||
' | `' +
|
||||
entry.path +
|
||||
'` | ' +
|
||||
entry.description +
|
||||
' | ' +
|
||||
defaultVal +
|
||||
' |';
|
||||
|
||||
if (section === 'features') {
|
||||
const stageVal = entry.stage ? '`' + entry.stage + '`' : '-';
|
||||
row += ' ' + stageVal + ' |';
|
||||
}
|
||||
|
||||
lines.push(row);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
@@ -277,6 +324,30 @@ function renderTableSections(sections: Map<string, DocEntry[]>) {
|
||||
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]) {
|
||||
const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
||||
if (entryUrl === import.meta.url) {
|
||||
|
||||
Reference in New Issue
Block a user