refactor(cli): implement noun-first labels and positive logic for settings (#20097)

This commit is contained in:
Keith Guerin
2026-02-23 15:52:04 -08:00
parent ba149afa0b
commit b44af7c168
33 changed files with 1092 additions and 742 deletions
+52 -51
View File
@@ -25,15 +25,16 @@ they appear in the UI.
| UI Label | Setting | Description | Default |
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
| Approval Mode | `general.approvalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Auto Update | `general.disableAutoUpdate` | Disable automatic updates. | `false` |
| Auto Update Notification | `general.disableUpdateNag` | Disable update notification prompts. | `false` |
| Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
| Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
| Chat History Period | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
### Output
@@ -47,30 +48,30 @@ they appear in the UI.
| ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
| Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Context Window Percentage | `ui.footer.hideContextPercentage` | Hide the context window remaining percentage. | `true` |
| Footer | `ui.hideFooter` | Hide the footer in the UI | `false` |
| Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, both, or nothing. | `"tips"` |
| Error Verbosity | `ui.errorVerbosity` | Controls whether recoverable errors are hidden (low) or fully shown (full). | `"low"` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
@@ -79,7 +80,7 @@ they appear in the UI.
| UI Label | Setting | Description | Default |
| -------- | ------------- | ---------------------------- | ------- |
| IDE Mode | `ide.enabled` | Enable IDE integration mode. | `false` |
| IDE | `ide.enabled` | Enable IDE integration mode. | `false` |
### Billing
@@ -94,41 +95,41 @@ they appear in the UI.
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
| Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
| Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
### Context
| UI Label | Setting | Description | Default |
| ------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Memory Discovery Max Dirs | `context.discoveryMaxDirs` | Maximum number of directories to search for memory. | `200` |
| Load Memory From Include Directories | `context.loadMemoryFromIncludeDirectories` | Controls how /memory refresh loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used. | `false` |
| Respect .gitignore | `context.fileFiltering.respectGitIgnore` | Respect .gitignore files when searching. | `true` |
| Respect .geminiignore | `context.fileFiltering.respectGeminiIgnore` | Respect .geminiignore files when searching. | `true` |
| Enable Recursive File Search | `context.fileFiltering.enableRecursiveFileSearch` | Enable recursive file search functionality when completing @ references in the prompt. | `true` |
| Enable Fuzzy Search | `context.fileFiltering.enableFuzzySearch` | Enable fuzzy search when searching for files. | `true` |
| ------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Discovery Max Dirs | `context.discoveryMaxDirs` | Maximum number of directories to search for memory. | `200` |
| Memory From Include Directories | `context.loadMemoryFromIncludeDirectories` | Controls how /memory refresh loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used. | `false` |
| .gitignore | `context.fileFiltering.respectGitIgnore` | Respect .gitignore files when searching. | `true` |
| .geminiignore | `context.fileFiltering.respectGeminiIgnore` | Respect .geminiignore files when searching. | `true` |
| Recursive File Search | `context.fileFiltering.enableRecursiveFileSearch` | Enable recursive file search functionality when completing @ references in the prompt. | `true` |
| Fuzzy Search | `context.fileFiltering.enableFuzzySearch` | Enable fuzzy search when searching for files. | `true` |
| Custom Ignore File Paths | `context.fileFiltering.customIgnoreFilePaths` | Additional ignore file paths to respect. These files take precedence over .geminiignore and .gitignore. Files earlier in the array take precedence over files later in the array, e.g. the first file takes precedence over the second one. | `[]` |
### Tools
| UI Label | Setting | Description | Default |
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
| -------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
| Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation. | `40000` |
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` |
| LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` |
### Security
| UI Label | Setting | Description | Default |
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
| Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
| Git Extensions | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
| Extension Source Regex Allowlist | `security.allowedExtensions` | List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting. | `[]` |
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `true` |
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
| Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
| Enable Context-Aware Security | `security.enableConseca` | Enable the context-aware security checker. This feature uses an LLM to dynamically generate and enforce security policies for tool use based on your prompt, providing an additional layer of protection against unintended actions. | `false` |
### Advanced
@@ -141,9 +142,9 @@ they appear in the UI.
| 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` |
| Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| 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` |
| 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` |
@@ -152,14 +153,14 @@ they appear in the UI.
### Skills
| UI Label | Setting | Description | Default |
| ------------------- | ---------------- | -------------------- | ------- |
| Enable Agent Skills | `skills.enabled` | Enable Agent Skills. | `true` |
| ------------ | ---------------- | -------------------- | ------- |
| Agent Skills | `skills.enabled` | Enable Agent Skills. | `true` |
### HooksConfig
| UI Label | Setting | Description | Default |
| ------------------ | --------------------------- | -------------------------------------------------------------------------------- | ------- |
| Enable Hooks | `hooksConfig.enabled` | Canonical toggle for the hooks system. When disabled, no hooks will be executed. | `true` |
| 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` |
<!-- SETTINGS-AUTOGEN:END -->
+49 -49
View File
@@ -207,9 +207,9 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Custom theme definitions.
- **Default:** `{}`
- **`ui.hideWindowTitle`** (boolean):
- **Description:** Hide the window title bar
- **Default:** `false`
- **`ui.windowTitle`** (boolean):
- **Description:** Show the window title bar
- **Default:** `true`
- **Requires restart:** Yes
- **`ui.inlineThinkingMode`** (enum):
@@ -238,43 +238,43 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **Requires restart:** Yes
- **`ui.hideTips`** (boolean):
- **Description:** Hide helpful tips in the UI
- **Default:** `false`
- **`ui.tips`** (boolean):
- **Description:** Show helpful tips in the UI
- **Default:** `true`
- **`ui.showShortcutsHint`** (boolean):
- **Description:** Show the "? for shortcuts" hint above the input.
- **Default:** `true`
- **`ui.hideBanner`** (boolean):
- **Description:** Hide the application banner
- **Default:** `false`
- **`ui.hideContextSummary`** (boolean):
- **Description:** Hide the context summary (GEMINI.md, MCP servers) above the
input.
- **Default:** `false`
- **`ui.footer.hideCWD`** (boolean):
- **Description:** Hide the current working directory path in the footer.
- **Default:** `false`
- **`ui.footer.hideSandboxStatus`** (boolean):
- **Description:** Hide the sandbox status indicator in the footer.
- **Default:** `false`
- **`ui.footer.hideModelInfo`** (boolean):
- **Description:** Hide the model name and context usage in the footer.
- **Default:** `false`
- **`ui.footer.hideContextPercentage`** (boolean):
- **Description:** Hides the context window remaining percentage.
- **`ui.banner`** (boolean):
- **Description:** Show the application banner
- **Default:** `true`
- **`ui.hideFooter`** (boolean):
- **Description:** Hide the footer from the UI
- **`ui.contextSummary`** (boolean):
- **Description:** Show the context summary (GEMINI.md, MCP servers) above the
input.
- **Default:** `true`
- **`ui.footer.cwd`** (boolean):
- **Description:** Show the current working directory path in the footer.
- **Default:** `true`
- **`ui.footer.sandboxStatus`** (boolean):
- **Description:** Show the sandbox status indicator in the footer.
- **Default:** `true`
- **`ui.footer.modelInfo`** (boolean):
- **Description:** Show the model name and context usage in the footer.
- **Default:** `true`
- **`ui.footer.contextPercentage`** (boolean):
- **Description:** Shows the context window remaining percentage.
- **Default:** `false`
- **`ui.footerEnabled`** (boolean):
- **Description:** Show the footer in the UI
- **Default:** `true`
- **`ui.showMemoryUsage`** (boolean):
- **Description:** Display memory usage information in the UI
- **Default:** `false`
@@ -395,15 +395,15 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `0.5`
- **Requires restart:** Yes
- **`model.disableLoopDetection`** (boolean):
- **Description:** Disable automatic detection and prevention of infinite
- **`model.loopDetection`** (boolean):
- **Description:** Enable automatic detection and prevention of infinite
loops.
- **Default:** `false`
- **Default:** `true`
- **Requires restart:** Yes
- **`model.skipNextSpeakerCheck`** (boolean):
- **Description:** Skip the next speaker check.
- **Default:** `true`
- **`model.nextSpeakerCheck`** (boolean):
- **Description:** Enable the next speaker check.
- **Default:** `false`
#### `modelConfigs`
@@ -824,11 +824,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `40000`
- **Requires restart:** Yes
- **`tools.disableLLMCorrection`** (boolean):
- **Description:** Disable LLM-based error correction for edit tools. When
enabled, tools will fail immediately if exact string matches are not found,
instead of attempting to self-correct.
- **Default:** `true`
- **`tools.llmCorrection`** (boolean):
- **Description:** Enable LLM-based error correction for edit tools. When
enabled, tools will attempt to self-correct if exact string matches are not
found.
- **Default:** `false`
- **Requires restart:** Yes
#### `mcp`
@@ -856,9 +856,9 @@ their corresponding top-level category object in your `settings.json` file.
#### `security`
- **`security.disableYoloMode`** (boolean):
- **Description:** Disable YOLO mode, even if enabled by a flag.
- **Default:** `false`
- **`security.yoloModeAllowed`** (boolean):
- **Description:** Allow YOLO mode to be used.
- **Default:** `true`
- **Requires restart:** Yes
- **`security.enablePermanentToolApproval`** (boolean):
@@ -866,15 +866,15 @@ their corresponding top-level category object in your `settings.json` file.
confirmation dialogs.
- **Default:** `false`
- **`security.blockGitExtensions`** (boolean):
- **Description:** Blocks installing and loading extensions from Git.
- **Default:** `false`
- **`security.gitExtensionsEnabled`** (boolean):
- **Description:** Allow installing and loading extensions from Git.
- **Default:** `true`
- **Requires restart:** Yes
- **`security.allowedExtensions`** (array):
- **Description:** List of Regex patterns for allowed extensions. If nonempty,
only extensions that match the patterns in this list are allowed. Overrides
the blockGitExtensions setting.
the gitExtensionsEnabled setting.
- **Default:** `[]`
- **Requires restart:** Yes
+14 -14
View File
@@ -1318,12 +1318,12 @@ describe('Approval mode tool exclusion logic', () => {
expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME); // Should be allowed in auto_edit
});
it('should throw an error if YOLO mode is attempted when disableYoloMode is true', async () => {
it('should throw an error if YOLO mode is attempted when yoloModeAllowed is false', async () => {
process.argv = ['node', 'script.js', '--yolo'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
security: {
disableYoloMode: true,
yoloModeAllowed: false,
},
});
@@ -1352,7 +1352,7 @@ describe('Approval mode tool exclusion logic', () => {
it('should fall back to default approval mode if plan mode is requested but not enabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: {
tools: {
defaultApprovalMode: 'plan',
},
experimental: {
@@ -1367,7 +1367,7 @@ describe('Approval mode tool exclusion logic', () => {
it('should allow plan approval mode if experimental plan is enabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: {
tools: {
defaultApprovalMode: 'plan',
},
experimental: {
@@ -2710,7 +2710,7 @@ describe('loadCliConfig approval mode', () => {
it('should use approvalMode from settings when no CLI flags are set', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: { defaultApprovalMode: 'auto_edit' },
tools: { defaultApprovalMode: 'auto_edit' },
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
@@ -2722,7 +2722,7 @@ describe('loadCliConfig approval mode', () => {
it('should prioritize --approval-mode flag over settings', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit'];
const settings = createTestMergedSettings({
general: { defaultApprovalMode: 'default' },
tools: { defaultApprovalMode: 'default' },
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
@@ -2734,7 +2734,7 @@ describe('loadCliConfig approval mode', () => {
it('should prioritize --yolo flag over settings', async () => {
process.argv = ['node', 'script.js', '--yolo'];
const settings = createTestMergedSettings({
general: { defaultApprovalMode: 'auto_edit' },
tools: { defaultApprovalMode: 'auto_edit' },
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
@@ -2744,7 +2744,7 @@ describe('loadCliConfig approval mode', () => {
it('should respect plan mode from settings when experimental.plan is enabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: { defaultApprovalMode: 'plan' },
tools: { defaultApprovalMode: 'plan' },
experimental: { plan: true },
});
const argv = await parseArguments(settings);
@@ -2755,7 +2755,7 @@ describe('loadCliConfig approval mode', () => {
it('should throw error if plan mode is in settings but experimental.plan is disabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: { defaultApprovalMode: 'plan' },
tools: { defaultApprovalMode: 'plan' },
experimental: { plan: false },
});
const argv = await parseArguments(settings);
@@ -3356,7 +3356,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
});
});
describe('loadCliConfig disableYoloMode', () => {
describe('loadCliConfig yoloModeAllowed', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
@@ -3377,17 +3377,17 @@ describe('loadCliConfig disableYoloMode', () => {
process.argv = ['node', 'script.js', '--approval-mode=auto_edit'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
security: { disableYoloMode: true },
security: { yoloModeAllowed: false },
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
});
it('should throw if YOLO mode is attempted when disableYoloMode is true', async () => {
it('should throw if YOLO mode is attempted when yoloModeAllowed is false', async () => {
process.argv = ['node', 'script.js', '--yolo'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
security: { disableYoloMode: true },
security: { yoloModeAllowed: false },
});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
@@ -3440,7 +3440,7 @@ describe('loadCliConfig secureModeEnabled', () => {
);
});
it('should set disableYoloMode to true when secureModeEnabled is true', async () => {
it('should set disableYoloMode to true in core config when secureModeEnabled is true', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
+12 -9
View File
@@ -544,8 +544,8 @@ export async function loadCliConfig(
const rawApprovalMode =
argv.approvalMode ||
(argv.yolo ? 'yolo' : undefined) ||
((settings.general?.defaultApprovalMode as string) !== 'yolo'
? settings.general?.defaultApprovalMode
((settings.tools?.defaultApprovalMode as string) !== 'yolo'
? settings.tools?.defaultApprovalMode
: undefined);
if (rawApprovalMode) {
@@ -578,8 +578,11 @@ export async function loadCliConfig(
approvalMode = ApprovalMode.DEFAULT;
}
// Override approval mode if disableYoloMode is set.
if (settings.security?.disableYoloMode || settings.admin?.secureModeEnabled) {
// Override approval mode if disableYoloMode is true.
if (
!settings.security?.yoloModeAllowed ||
settings.admin?.secureModeEnabled
) {
if (approvalMode === ApprovalMode.YOLO) {
if (settings.admin?.secureModeEnabled) {
debugLogger.error(
@@ -587,7 +590,7 @@ export async function loadCliConfig(
);
} else {
debugLogger.error(
'YOLO mode is disabled by the "disableYolo" setting.',
'YOLO mode is disabled by the "yoloModeAllowed" setting.',
);
}
throw new FatalConfigError(
@@ -797,7 +800,7 @@ export async function loadCliConfig(
geminiMdFilePaths: filePaths,
approvalMode,
disableYoloMode:
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
!settings.security?.yoloModeAllowed || settings.admin?.secureModeEnabled,
showMemoryUsage: settings.ui?.showMemoryUsage || false,
accessibility: {
...settings.ui?.accessibility,
@@ -837,7 +840,7 @@ export async function loadCliConfig(
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode,
disableLoopDetection: settings.model?.disableLoopDetection,
disableLoopDetection: !settings.model?.loopDetection,
compressionThreshold: settings.model?.compressionThreshold,
folderTrust,
interactive,
@@ -849,7 +852,7 @@ export async function loadCliConfig(
shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout,
enableShellOutputEfficiency:
settings.tools?.shell?.enableShellOutputEfficiency ?? true,
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
skipNextSpeakerCheck: !settings.model?.nextSpeakerCheck,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
eventEmitter: coreEvents,
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
@@ -863,7 +866,7 @@ export async function loadCliConfig(
retryFetchErrors: settings.general?.retryFetchErrors,
maxAttempts: settings.general?.maxAttempts,
ptyInfo: ptyInfo?.name,
disableLLMCorrection: settings.tools?.disableLLMCorrection,
disableLLMCorrection: !settings.tools?.llmCorrection,
rawOutput: argv.rawOutput,
acceptRawOutputRisk: argv.acceptRawOutputRisk,
modelConfigServiceConfig: settings.modelConfigs,
@@ -50,7 +50,7 @@ describe('ExtensionManager theme loading', () => {
extensionManager = new ExtensionManager({
settings: createTestMergedSettings({
experimental: { extensionConfig: true },
security: { blockGitExtensions: false },
security: { gitExtensionsEnabled: true },
admin: { extensions: { enabled: true }, mcp: { enabled: true } },
}),
requestConsent: async () => true,
+2 -2
View File
@@ -177,7 +177,7 @@ export class ExtensionManager extends ExtensionLoader {
} else if (
(installMetadata.type === 'git' ||
installMetadata.type === 'github-release') &&
this.settings.security.blockGitExtensions
!this.settings.security.gitExtensionsEnabled
) {
throw new Error(
'Installing extensions from remote sources is disallowed by your current settings.',
@@ -655,7 +655,7 @@ Would you like to attempt to install via "git clone" instead?`,
} else if (
(installMetadata?.type === 'git' ||
installMetadata?.type === 'github-release') &&
this.settings.security.blockGitExtensions
!this.settings.security.gitExtensionsEnabled
) {
debugLogger.warn(
`Failed to load extension ${extensionDir}. Extensions from remote sources is disallowed by your current settings.`,
+8 -8
View File
@@ -752,7 +752,7 @@ name = "yolo-checker"
consoleSpy.mockRestore();
});
it('should not load github extensions if blockGitExtensions is set', async () => {
it('should not load github extensions if gitExtensionsEnabled is false', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createExtension({
extensionsDir: userExtensionsDir,
@@ -764,14 +764,14 @@ name = "yolo-checker"
},
});
const blockGitExtensionsSetting = createTestMergedSettings({
security: { blockGitExtensions: true },
const gitExtensionsSetting = createTestMergedSettings({
security: { gitExtensionsEnabled: false },
});
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: blockGitExtensionsSetting,
settings: gitExtensionsSetting,
});
const extensions = await extensionManager.loadExtensions();
const extension = extensions.find((e) => e.name === 'my-ext');
@@ -1294,16 +1294,16 @@ name = "yolo-checker"
fs.rmSync(targetExtDir, { recursive: true, force: true });
});
it('should not install a github extension if blockGitExtensions is set', async () => {
it('should not install a github extension if gitExtensionsEnabled is false', async () => {
const gitUrl = 'https://somehost.com/somerepo.git';
const blockGitExtensionsSetting = createTestMergedSettings({
security: { blockGitExtensions: true },
const gitExtensionsSetting = createTestMergedSettings({
security: { gitExtensionsEnabled: false },
});
extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings,
settings: blockGitExtensionsSetting,
settings: gitExtensionsSetting,
});
await extensionManager.loadExtensions();
await expect(
@@ -35,7 +35,7 @@ describe('settings-validation', () => {
const invalidSettings = {
model: {
name: {
skipNextSpeakerCheck: true,
nextSpeakerCheck: false,
},
},
};
@@ -116,10 +116,10 @@ describe('settings-validation', () => {
const validSettings = {
ui: {
theme: 'dark',
hideWindowTitle: true,
windowTitle: false,
footer: {
hideCWD: false,
hideModelInfo: true,
cwd: true,
modelInfo: false,
},
},
tools: {
@@ -305,7 +305,7 @@ describe('settings-validation', () => {
const invalidSettings = {
model: {
name: {
skipNextSpeakerCheck: true,
nextSpeakerCheck: false,
},
},
};
+113 -64
View File
@@ -503,21 +503,21 @@ describe('Settings Loading and Merging', () => {
expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used
});
it('should not allow user or workspace to override system disableYoloMode', () => {
it('should not allow user or workspace to override system yoloModeAllowed', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
const userSettingsContent = {
security: {
disableYoloMode: false,
yoloModeAllowed: true,
},
};
const workspaceSettingsContent = {
security: {
disableYoloMode: false, // This should be ignored
yoloModeAllowed: true, // This should be ignored
},
};
const systemSettingsContent = {
security: {
disableYoloMode: true,
yoloModeAllowed: false,
},
};
@@ -534,7 +534,7 @@ describe('Settings Loading and Merging', () => {
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used
expect(settings.merged.security?.yoloModeAllowed).toBe(false); // System setting should be used
});
it.each([
@@ -1948,9 +1948,8 @@ describe('Settings Loading and Merging', () => {
},
);
const setValueSpy = vi.spyOn(LoadedSettings.prototype, 'setValue');
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
setValueSpy.mockClear();
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings, true);
@@ -1972,8 +1971,8 @@ describe('Settings Loading and Merging', () => {
},
);
const setValueSpy = vi.spyOn(LoadedSettings.prototype, 'setValue');
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings, true);
@@ -1985,7 +1984,7 @@ describe('Settings Loading and Merging', () => {
);
});
it('should migrate tools.approvalMode to general.defaultApprovalMode', () => {
it('should migrate tools.approvalMode to tools.defaultApprovalMode', () => {
const userSettingsContent = {
tools: {
approvalMode: 'plan',
@@ -2000,14 +1999,14 @@ describe('Settings Loading and Merging', () => {
},
);
const setValueSpy = vi.spyOn(LoadedSettings.prototype, 'setValue');
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings, true);
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'general',
'tools',
expect.objectContaining({ defaultApprovalMode: 'plan' }),
);
@@ -2019,22 +2018,44 @@ describe('Settings Loading and Merging', () => {
);
});
it('should migrate all 4 inverted boolean settings', () => {
it('should migrate all inverted boolean settings to positive logic', () => {
const userSettingsContent = {
general: {
disableAutoUpdate: false,
disableUpdateNag: true,
},
ui: {
hideWindowTitle: true,
hideTips: false,
hideBanner: true,
hideContextSummary: false,
hideFooter: true,
footer: {
hideCWD: true,
hideSandboxStatus: false,
hideModelInfo: true,
hideContextPercentage: false,
},
accessibility: {
disableLoadingPhrases: true,
},
},
model: {
disableLoopDetection: true,
skipNextSpeakerCheck: false,
},
tools: {
disableLLMCorrection: true,
},
security: {
yoloModeAllowed: false,
blockGitExtensions: false,
},
context: {
fileFiltering: {
disableFuzzySearch: false,
},
},
ui: {
accessibility: {
disableLoadingPhrases: true,
},
},
};
(fs.readFileSync as Mock).mockImplementation(
@@ -2045,49 +2066,77 @@ describe('Settings Loading and Merging', () => {
},
);
const setValueSpy = vi.spyOn(LoadedSettings.prototype, 'setValue');
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings, true);
// Check that general settings were migrated with inverted values
// Verify general migrations
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'general',
expect.objectContaining({ enableAutoUpdate: true }),
);
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'general',
expect.objectContaining({ enableAutoUpdateNotification: false }),
expect.objectContaining({
enableAutoUpdate: true,
enableAutoUpdateNotification: false,
}),
);
// Check context.fileFiltering was migrated
// Verify UI migrations
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'ui',
expect.objectContaining({
windowTitle: false,
tips: true,
banner: false,
contextSummary: true,
footerEnabled: false,
footer: expect.objectContaining({
cwd: false,
sandboxStatus: true,
modelInfo: false,
contextPercentage: true,
}),
}),
);
// Verify model migrations
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'model',
expect.objectContaining({
loopDetection: false,
nextSpeakerCheck: true,
}),
);
// Verify tools migrations
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'tools',
expect.objectContaining({
llmCorrection: false,
}),
);
// Verify security migrations
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'security',
expect.objectContaining({
yoloModeAllowed: false,
gitExtensionsEnabled: true,
}),
);
// Verify context migrations
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'context',
expect.objectContaining({
fileFiltering: expect.objectContaining({ enableFuzzySearch: true }),
fileFiltering: expect.objectContaining({
enableFuzzySearch: true,
}),
);
// Check ui.accessibility was migrated
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'ui',
expect.objectContaining({
accessibility: expect.objectContaining({
enableLoadingPhrases: false,
}),
}),
);
// Check that enableLoadingPhrases: false was further migrated to loadingPhrases: 'off'
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'ui',
expect.objectContaining({
loadingPhrases: 'off',
}),
);
});
@@ -2339,8 +2388,8 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR);
// Verify it was migrated in memory
expect(settings.system.settings.agents?.overrides).toMatchObject({
codebase_investigator: {
expect(settings.system.settings.experimental).toMatchObject({
codebaseInvestigator: {
enabled: true,
},
});
@@ -2360,7 +2409,7 @@ describe('Settings Loading and Merging', () => {
);
});
it('should migrate experimental agent settings to agents overrides', () => {
it('should migrate experimental agent settings to new experimental keys', () => {
const userSettingsContent = {
experimental: {
codebaseInvestigatorSettings: {
@@ -2387,9 +2436,9 @@ describe('Settings Loading and Merging', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR);
// Verify migration to agents.overrides
expect(settings.user.settings.agents?.overrides).toMatchObject({
codebase_investigator: {
// Verify migration to new experimental keys
expect(settings.user.settings.experimental).toMatchObject({
codebaseInvestigator: {
enabled: true,
runConfig: {
maxTurns: 15,
@@ -2404,7 +2453,7 @@ describe('Settings Loading and Merging', () => {
},
},
},
cli_help: {
cliHelp: {
enabled: false,
},
});
@@ -2988,7 +3037,6 @@ MALICIOUS_VAR=allowed-because-trusted
});
});
});
});
describe('LoadedSettings Isolation and Serializability', () => {
let loadedSettings: LoadedSettings;
@@ -3024,9 +3072,9 @@ describe('LoadedSettings Isolation and Serializability', () => {
loadedSettings.setValue(SettingScope.User, 'test', complexValue);
const userSettings = loadedSettings.forScope(SettingScope.User);
const settingsValue = (userSettings.settings as Record<string, unknown>)[
'test'
] as TestData;
const settingsValue = (
userSettings.settings as Record<string, unknown>
)['test'] as TestData;
const originalValue = (
userSettings.originalSettings as Record<string, unknown>
)['test'] as TestData;
@@ -3081,9 +3129,9 @@ describe('LoadedSettings Isolation and Serializability', () => {
loadedSettings.setValue(SettingScope.User, 'test', mapValue);
const userSettings = loadedSettings.forScope(SettingScope.User);
const settingsValue = (userSettings.settings as Record<string, unknown>)[
'test'
] as { myMap: Map<string, string> };
const settingsValue = (
userSettings.settings as Record<string, unknown>
)['test'] as { myMap: Map<string, string> };
// Map is preserved by structuredClone
expect(settingsValue.myMap).toBeInstanceOf(Map);
@@ -3105,3 +3153,4 @@ describe('LoadedSettings Isolation and Serializability', () => {
});
});
});
});
+270 -102
View File
@@ -791,7 +791,7 @@ export function loadSettings(
);
// Automatically migrate deprecated settings when loading.
migrateDeprecatedSettings(loadedSettings);
migrateDeprecatedSettings(loadedSettings, true);
return loadedSettings;
}
@@ -820,6 +820,7 @@ export function migrateDeprecatedSettings(
newKey: string,
prefix: string,
foundDeprecated?: string[],
invert = false,
): boolean => {
let modified = false;
const oldValue = settings[oldKey];
@@ -836,8 +837,8 @@ export function migrateDeprecatedSettings(
modified = true;
}
} else {
// Only old exists, migrate to new (inverted)
settings[newKey] = !oldValue;
// Only old exists, migrate to new
settings[newKey] = invert ? !oldValue : oldValue;
if (removeDeprecated) {
delete settings[oldKey];
}
@@ -867,6 +868,7 @@ export function migrateDeprecatedSettings(
'enableAutoUpdate',
'general',
foundDeprecated,
true,
) || modified;
modified =
migrateBoolean(
@@ -875,6 +877,7 @@ export function migrateDeprecatedSettings(
'enableAutoUpdateNotification',
'general',
foundDeprecated,
true,
) || modified;
if (modified) {
@@ -889,6 +892,107 @@ export function migrateDeprecatedSettings(
const uiSettings = settings.ui as Record<string, unknown> | undefined;
if (uiSettings) {
const newUi = { ...uiSettings };
let modified = false;
// Positive logic migrations
modified =
migrateBoolean(
newUi,
'hideWindowTitle',
'windowTitle',
'ui',
foundDeprecated,
true,
) || modified;
modified =
migrateBoolean(
newUi,
'hideTips',
'tips',
'ui',
foundDeprecated,
true,
) || modified;
modified =
migrateBoolean(
newUi,
'hideBanner',
'banner',
'ui',
foundDeprecated,
true,
) || modified;
modified =
migrateBoolean(
newUi,
'hideContextSummary',
'contextSummary',
'ui',
foundDeprecated,
true,
) || modified;
modified =
migrateBoolean(
newUi,
'hideFooter',
'footerEnabled',
'ui',
foundDeprecated,
true,
) || modified;
// Footer migrations
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const footerSettings = newUi['footer'] as
| Record<string, unknown>
| undefined;
if (footerSettings) {
const newFooter = { ...footerSettings };
let footerModified = false;
footerModified =
migrateBoolean(
newFooter,
'hideCWD',
'cwd',
'ui.footer',
foundDeprecated,
true,
) || footerModified;
footerModified =
migrateBoolean(
newFooter,
'hideSandboxStatus',
'sandboxStatus',
'ui.footer',
foundDeprecated,
true,
) || footerModified;
footerModified =
migrateBoolean(
newFooter,
'hideModelInfo',
'modelInfo',
'ui.footer',
foundDeprecated,
true,
) || footerModified;
footerModified =
migrateBoolean(
newFooter,
'hideContextPercentage',
'contextPercentage',
'ui.footer',
foundDeprecated,
true,
) || footerModified;
if (footerModified) {
newUi['footer'] = newFooter;
modified = true;
}
}
// Accessibility migrations
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const accessibilitySettings = newUi['accessibility'] as
| Record<string, unknown>
@@ -903,13 +1007,11 @@ export function migrateDeprecatedSettings(
'enableLoadingPhrases',
'ui.accessibility',
foundDeprecated,
true,
)
) {
newUi['accessibility'] = newAccessibility;
loadedSettings.setValue(scope, 'ui', newUi);
if (!settingsFile.readOnly) {
anyModified = true;
}
modified = true;
}
// Migrate enableLoadingPhrases: false → loadingPhrases: 'off'
@@ -920,12 +1022,121 @@ export function migrateDeprecatedSettings(
) {
if (!enableLP) {
newUi['loadingPhrases'] = 'off';
modified = true;
}
foundDeprecated.push('ui.accessibility.enableLoadingPhrases');
}
}
if (modified) {
loadedSettings.setValue(scope, 'ui', newUi);
if (!settingsFile.readOnly) {
anyModified = true;
}
}
foundDeprecated.push('ui.accessibility.enableLoadingPhrases');
}
// Migrate model settings
const modelSettings = settings.model as Record<string, unknown> | undefined;
if (modelSettings) {
const newModel = { ...modelSettings };
let modified = false;
modified =
migrateBoolean(
newModel,
'disableLoopDetection',
'loopDetection',
'model',
foundDeprecated,
true,
) || modified;
modified =
migrateBoolean(
newModel,
'skipNextSpeakerCheck',
'nextSpeakerCheck',
'model',
foundDeprecated,
true,
) || modified;
if (modified) {
loadedSettings.setValue(scope, 'model', newModel);
if (!settingsFile.readOnly) {
anyModified = true;
}
}
}
// Migrate tools settings
const toolsSettings = settings.tools as Record<string, unknown> | undefined;
if (toolsSettings) {
const newTools = { ...toolsSettings };
let modified = false;
modified =
migrateBoolean(
newTools,
'disableLLMCorrection',
'llmCorrection',
'tools',
foundDeprecated,
true,
) || modified;
if (toolsSettings['approvalMode'] !== undefined) {
foundDeprecated.push('tools.approvalMode');
// Map tools.approvalMode to tools.defaultApprovalMode
if (newTools['defaultApprovalMode'] === undefined) {
newTools['defaultApprovalMode'] = toolsSettings['approvalMode'];
modified = true;
}
if (removeDeprecated) {
delete newTools['approvalMode'];
modified = true;
}
}
if (modified) {
loadedSettings.setValue(scope, 'tools', newTools);
if (!settingsFile.readOnly) {
anyModified = true;
}
}
}
// Migrate security settings
const securitySettings = settings.security as
| Record<string, unknown>
| undefined;
if (securitySettings) {
const newSecurity = { ...securitySettings };
let modified = false;
modified =
migrateBoolean(
newSecurity,
'disableYoloMode',
'yoloModeAllowed',
'security',
foundDeprecated,
true,
) || modified;
modified =
migrateBoolean(
newSecurity,
'blockGitExtensions',
'gitExtensionsEnabled',
'security',
foundDeprecated,
true,
) || modified;
if (modified) {
loadedSettings.setValue(scope, 'security', newSecurity);
if (!settingsFile.readOnly) {
anyModified = true;
}
}
}
@@ -936,6 +1147,7 @@ export function migrateDeprecatedSettings(
| undefined;
if (contextSettings) {
const newContext = { ...contextSettings };
let modified = false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const fileFilteringSettings = newContext['fileFiltering'] as
| Record<string, unknown>
@@ -950,46 +1162,21 @@ export function migrateDeprecatedSettings(
'enableFuzzySearch',
'context.fileFiltering',
foundDeprecated,
true,
)
) {
newContext['fileFiltering'] = newFileFiltering;
modified = true;
}
}
if (modified) {
loadedSettings.setValue(scope, 'context', newContext);
if (!settingsFile.readOnly) {
anyModified = true;
}
}
}
}
// Migrate tools settings
const toolsSettings = settings.tools as Record<string, unknown> | undefined;
if (toolsSettings) {
if (toolsSettings['approvalMode'] !== undefined) {
foundDeprecated.push('tools.approvalMode');
const generalSettings =
(settings.general as Record<string, unknown> | undefined) || {};
const newGeneral = { ...generalSettings };
// Only set defaultApprovalMode if it's not already set
if (newGeneral['defaultApprovalMode'] === undefined) {
newGeneral['defaultApprovalMode'] = toolsSettings['approvalMode'];
loadedSettings.setValue(scope, 'general', newGeneral);
if (!settingsFile.readOnly) {
anyModified = true;
}
}
if (removeDeprecated) {
const newTools = { ...toolsSettings };
delete newTools['approvalMode'];
loadedSettings.setValue(scope, 'tools', newTools);
if (!settingsFile.readOnly) {
anyModified = true;
}
}
}
}
// Migrate experimental agent settings
const experimentalModified = migrateExperimentalSettings(
@@ -998,6 +1185,7 @@ export function migrateDeprecatedSettings(
scope,
removeDeprecated,
foundDeprecated,
settingsFile.readOnly ?? false,
);
if (experimentalModified) {
@@ -1076,75 +1264,62 @@ function migrateExperimentalSettings(
loadedSettings: LoadedSettings,
scope: LoadableSettingScope,
removeDeprecated: boolean,
foundDeprecated?: string[],
foundDeprecated: string[] | undefined,
readOnly: boolean,
): boolean {
const experimentalSettings = settings.experimental as
| Record<string, unknown>
| undefined;
if (experimentalSettings) {
const agentsSettings = {
...(settings.agents as Record<string, unknown> | undefined),
};
const agentsOverrides = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...((agentsSettings['overrides'] as Record<string, unknown>) || {}),
};
const newExperimental = { ...experimentalSettings };
let modified = false;
const migrateExperimental = (
oldKey: string,
migrateFn: (oldValue: Record<string, unknown>) => void,
) => {
const old = experimentalSettings[oldKey];
const migrateAgent = (oldKey: string, newKey: string) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const old = newExperimental[oldKey] as
| Record<string, unknown>
| undefined;
if (old) {
foundDeprecated?.push(`experimental.${oldKey}`);
const override =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
migrateFn(old as Record<string, unknown>);
modified = true;
}
};
// Migrate codebaseInvestigatorSettings -> agents.overrides.codebase_investigator
migrateExperimental('codebaseInvestigatorSettings', (old) => {
const override = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(agentsOverrides['codebase_investigator'] as
| Record<string, unknown>
| undefined),
};
(newExperimental[newKey] as Record<string, unknown>) || {};
if (old['enabled'] !== undefined) override['enabled'] = old['enabled'];
const runConfig = {
if (
old['maxNumTurns'] !== undefined ||
old['maxTimeMinutes'] !== undefined
) {
const runConfig =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(override['runConfig'] as Record<string, unknown> | undefined),
};
(override['runConfig'] as Record<string, unknown>) || {};
if (old['maxNumTurns'] !== undefined)
runConfig['maxTurns'] = old['maxNumTurns'];
if (old['maxTimeMinutes'] !== undefined)
runConfig['maxTimeMinutes'] = old['maxTimeMinutes'];
if (Object.keys(runConfig).length > 0) override['runConfig'] = runConfig;
override['runConfig'] = runConfig;
}
if (old['model'] !== undefined || old['thinkingBudget'] !== undefined) {
const modelConfig = {
const modelConfig =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(override['modelConfig'] as Record<string, unknown> | undefined),
};
(override['modelConfig'] as Record<string, unknown>) || {};
if (old['model'] !== undefined) modelConfig['model'] = old['model'];
if (old['thinkingBudget'] !== undefined) {
const generateContentConfig = {
const generateContentConfig =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(modelConfig['generateContentConfig'] as
| Record<string, unknown>
| undefined),
};
const thinkingConfig = {
(modelConfig['generateContentConfig'] as Record<
string,
unknown
>) || {};
const thinkingConfig =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(generateContentConfig['thinkingConfig'] as
| Record<string, unknown>
| undefined),
};
(generateContentConfig['thinkingConfig'] as Record<
string,
unknown
>) || {};
thinkingConfig['thinkingBudget'] = old['thinkingBudget'];
generateContentConfig['thinkingConfig'] = thinkingConfig;
modelConfig['generateContentConfig'] = generateContentConfig;
@@ -1152,31 +1327,24 @@ function migrateExperimentalSettings(
override['modelConfig'] = modelConfig;
}
agentsOverrides['codebase_investigator'] = override;
});
// Migrate cliHelpAgentSettings -> agents.overrides.cli_help
migrateExperimental('cliHelpAgentSettings', (old) => {
const override = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(agentsOverrides['cli_help'] as Record<string, unknown> | undefined),
};
if (old['enabled'] !== undefined) override['enabled'] = old['enabled'];
agentsOverrides['cli_help'] = override;
});
if (modified) {
agentsSettings['overrides'] = agentsOverrides;
loadedSettings.setValue(scope, 'agents', agentsSettings);
newExperimental[newKey] = override;
if (removeDeprecated) {
const newExperimental = { ...experimentalSettings };
delete newExperimental['codebaseInvestigatorSettings'];
delete newExperimental['cliHelpAgentSettings'];
loadedSettings.setValue(scope, 'experimental', newExperimental);
delete newExperimental[oldKey];
}
modified = true;
}
};
migrateAgent('codebaseInvestigatorSettings', 'codebaseInvestigator');
migrateAgent('cliHelpAgentSettings', 'cliHelp');
if (modified) {
loadedSettings.setValue(scope, 'experimental', newExperimental);
if (!readOnly) {
return true;
}
}
}
return false;
}
@@ -199,8 +199,8 @@ describe('SettingsSchema', () => {
getSettingsSchema().ui.properties.showMemoryUsage.showInDialog,
).toBe(true);
expect(
getSettingsSchema().ui.properties.footer.properties
.hideContextPercentage.showInDialog,
getSettingsSchema().ui.properties.footer.properties.contextPercentage
.showInDialog,
).toBe(true);
expect(getSettingsSchema().general.properties.vimMode.showInDialog).toBe(
true,
@@ -211,18 +211,14 @@ describe('SettingsSchema', () => {
expect(
getSettingsSchema().general.properties.enableAutoUpdate.showInDialog,
).toBe(true);
expect(
getSettingsSchema().ui.properties.hideWindowTitle.showInDialog,
).toBe(true);
expect(getSettingsSchema().ui.properties.hideTips.showInDialog).toBe(
expect(getSettingsSchema().ui.properties.windowTitle.showInDialog).toBe(
true,
);
expect(getSettingsSchema().ui.properties.tips.showInDialog).toBe(true);
expect(
getSettingsSchema().ui.properties.showShortcutsHint.showInDialog,
).toBe(true);
expect(getSettingsSchema().ui.properties.hideBanner.showInDialog).toBe(
true,
);
expect(getSettingsSchema().ui.properties.banner.showInDialog).toBe(true);
expect(
getSettingsSchema().privacy.properties.usageStatisticsEnabled
.showInDialog,
+127 -65
View File
@@ -130,6 +130,24 @@ export interface SettingsSchema {
export type MemoryImportFormat = 'tree' | 'flat';
export type DnsResolutionOrder = 'ipv4first' | 'verbatim';
/**
* The canonical order for setting categories.
*/
export const SETTING_CATEGORY_ORDER = [
'General',
'UI',
'Model',
'Context',
'Tools',
'IDE',
'Privacy',
'Extensions',
'Security',
'Experimental',
'Admin',
'Advanced',
] as const;
/**
* The canonical schema for all settings.
* The structure of this object defines the structure of the `Settings` type.
@@ -191,9 +209,9 @@ const SETTINGS_SCHEMA = {
description: 'Enable Vim keybindings',
showInDialog: true,
},
defaultApprovalMode: {
approvalMode: {
type: 'enum',
label: 'Default Approval Mode',
label: 'Approval Mode',
category: 'General',
requiresRestart: false,
default: 'default',
@@ -218,27 +236,27 @@ const SETTINGS_SCHEMA = {
description: 'Enable DevTools inspector on launch.',
showInDialog: false,
},
enableAutoUpdate: {
disableAutoUpdate: {
type: 'boolean',
label: 'Enable Auto Update',
label: 'Auto Update',
category: 'General',
requiresRestart: false,
default: true,
description: 'Enable automatic updates.',
default: false,
description: 'Disable automatic updates.',
showInDialog: true,
},
enableAutoUpdateNotification: {
disableUpdateNag: {
type: 'boolean',
label: 'Enable Auto Update Notification',
label: 'Auto Update Notification',
category: 'General',
requiresRestart: false,
default: true,
description: 'Enable update notification prompts.',
default: false,
description: 'Disable update notification prompts.',
showInDialog: false,
},
enableNotifications: {
type: 'boolean',
label: 'Enable Notifications',
label: 'Notifications',
category: 'General',
requiresRestart: false,
default: false,
@@ -257,7 +275,7 @@ const SETTINGS_SCHEMA = {
properties: {
enabled: {
type: 'boolean',
label: 'Enable Checkpointing',
label: 'Checkpointing',
category: 'General',
requiresRestart: true,
default: false,
@@ -336,7 +354,7 @@ const SETTINGS_SCHEMA = {
properties: {
enabled: {
type: 'boolean',
label: 'Enable Session Cleanup',
label: 'Session Cleanup',
category: 'General',
requiresRestart: false,
default: false,
@@ -345,7 +363,7 @@ const SETTINGS_SCHEMA = {
},
maxAge: {
type: 'string',
label: 'Keep chat history',
label: 'Chat History Period',
category: 'General',
requiresRestart: false,
default: undefined as string | undefined,
@@ -466,7 +484,7 @@ const SETTINGS_SCHEMA = {
},
hideWindowTitle: {
type: 'boolean',
label: 'Hide Window Title',
label: 'Window Title',
category: 'UI',
requiresRestart: true,
default: false,
@@ -488,7 +506,7 @@ const SETTINGS_SCHEMA = {
},
showStatusInTitle: {
type: 'boolean',
label: 'Show Thoughts in Title',
label: 'Thoughts in Title',
category: 'UI',
requiresRestart: false,
default: false,
@@ -508,7 +526,7 @@ const SETTINGS_SCHEMA = {
},
showHomeDirectoryWarning: {
type: 'boolean',
label: 'Show Home Directory Warning',
label: 'Home Directory Warning',
category: 'UI',
requiresRestart: true,
default: true,
@@ -518,7 +536,7 @@ const SETTINGS_SCHEMA = {
},
showCompatibilityWarnings: {
type: 'boolean',
label: 'Show Compatibility Warnings',
label: 'Compatibility Warnings',
category: 'UI',
requiresRestart: true,
default: true,
@@ -527,7 +545,7 @@ const SETTINGS_SCHEMA = {
},
hideTips: {
type: 'boolean',
label: 'Hide Tips',
label: 'Tips',
category: 'UI',
requiresRestart: false,
default: false,
@@ -536,7 +554,7 @@ const SETTINGS_SCHEMA = {
},
showShortcutsHint: {
type: 'boolean',
label: 'Show Shortcuts Hint',
label: 'Shortcuts Hint',
category: 'UI',
requiresRestart: false,
default: true,
@@ -545,7 +563,7 @@ const SETTINGS_SCHEMA = {
},
hideBanner: {
type: 'boolean',
label: 'Hide Banner',
label: 'Banner',
category: 'UI',
requiresRestart: false,
default: false,
@@ -554,7 +572,7 @@ const SETTINGS_SCHEMA = {
},
hideContextSummary: {
type: 'boolean',
label: 'Hide Context Summary',
label: 'Context Summary',
category: 'UI',
requiresRestart: false,
default: false,
@@ -573,7 +591,7 @@ const SETTINGS_SCHEMA = {
properties: {
hideCWD: {
type: 'boolean',
label: 'Hide CWD',
label: 'CWD',
category: 'UI',
requiresRestart: false,
default: false,
@@ -583,7 +601,7 @@ const SETTINGS_SCHEMA = {
},
hideSandboxStatus: {
type: 'boolean',
label: 'Hide Sandbox Status',
label: 'Sandbox Status',
category: 'UI',
requiresRestart: false,
default: false,
@@ -592,7 +610,7 @@ const SETTINGS_SCHEMA = {
},
hideModelInfo: {
type: 'boolean',
label: 'Hide Model Info',
label: 'Model Info',
category: 'UI',
requiresRestart: false,
default: false,
@@ -601,27 +619,27 @@ const SETTINGS_SCHEMA = {
},
hideContextPercentage: {
type: 'boolean',
label: 'Hide Context Window Percentage',
label: 'Context Window Percentage',
category: 'UI',
requiresRestart: false,
default: true,
description: 'Hides the context window remaining percentage.',
description: 'Hide the context window remaining percentage.',
showInDialog: true,
},
},
},
hideFooter: {
type: 'boolean',
label: 'Hide Footer',
label: 'Footer',
category: 'UI',
requiresRestart: false,
default: false,
description: 'Hide the footer from the UI',
description: 'Hide the footer in the UI',
showInDialog: true,
},
showMemoryUsage: {
type: 'boolean',
label: 'Show Memory Usage',
label: 'Memory Usage',
category: 'UI',
requiresRestart: false,
default: false,
@@ -630,7 +648,7 @@ const SETTINGS_SCHEMA = {
},
showLineNumbers: {
type: 'boolean',
label: 'Show Line Numbers',
label: 'Line Numbers',
category: 'UI',
requiresRestart: false,
default: true,
@@ -639,7 +657,7 @@ const SETTINGS_SCHEMA = {
},
showCitations: {
type: 'boolean',
label: 'Show Citations',
label: 'Citations',
category: 'UI',
requiresRestart: false,
default: false,
@@ -648,7 +666,7 @@ const SETTINGS_SCHEMA = {
},
showModelInfoInChat: {
type: 'boolean',
label: 'Show Model Info In Chat',
label: 'Model Info In Chat',
category: 'UI',
requiresRestart: false,
default: false,
@@ -657,7 +675,7 @@ const SETTINGS_SCHEMA = {
},
showUserIdentity: {
type: 'boolean',
label: 'Show User Identity',
label: 'User Identity',
category: 'UI',
requiresRestart: false,
default: true,
@@ -667,7 +685,7 @@ const SETTINGS_SCHEMA = {
},
useAlternateBuffer: {
type: 'boolean',
label: 'Use Alternate Screen Buffer',
label: 'Alternate Screen Buffer',
category: 'UI',
requiresRestart: true,
default: false,
@@ -677,7 +695,7 @@ const SETTINGS_SCHEMA = {
},
useBackgroundColor: {
type: 'boolean',
label: 'Use Background Color',
label: 'Background Color',
category: 'UI',
requiresRestart: false,
default: true,
@@ -696,7 +714,7 @@ const SETTINGS_SCHEMA = {
},
showSpinner: {
type: 'boolean',
label: 'Show Spinner',
label: 'Spinner',
category: 'UI',
requiresRestart: false,
default: true,
@@ -791,7 +809,7 @@ const SETTINGS_SCHEMA = {
properties: {
enabled: {
type: 'boolean',
label: 'IDE Mode',
label: 'IDE',
category: 'IDE',
requiresRestart: true,
default: false,
@@ -800,7 +818,7 @@ const SETTINGS_SCHEMA = {
},
hasSeenNudge: {
type: 'boolean',
label: 'Has Seen IDE Integration Nudge',
label: 'IDE Integration Nudge',
category: 'IDE',
requiresRestart: false,
default: false,
@@ -821,7 +839,7 @@ const SETTINGS_SCHEMA = {
properties: {
usageStatisticsEnabled: {
type: 'boolean',
label: 'Enable Usage Statistics',
label: 'Usage Statistics',
category: 'Privacy',
requiresRestart: true,
default: true,
@@ -933,7 +951,7 @@ const SETTINGS_SCHEMA = {
},
disableLoopDetection: {
type: 'boolean',
label: 'Disable Loop Detection',
label: 'Loop Detection',
category: 'Model',
requiresRestart: true,
default: false,
@@ -943,7 +961,7 @@ const SETTINGS_SCHEMA = {
},
skipNextSpeakerCheck: {
type: 'boolean',
label: 'Skip Next Speaker Check',
label: 'Next Speaker Check',
category: 'Model',
requiresRestart: false,
default: true,
@@ -1116,7 +1134,7 @@ const SETTINGS_SCHEMA = {
},
includeDirectoryTree: {
type: 'boolean',
label: 'Include Directory Tree',
label: 'Directory Tree',
category: 'Context',
requiresRestart: false,
default: true,
@@ -1126,7 +1144,7 @@ const SETTINGS_SCHEMA = {
},
discoveryMaxDirs: {
type: 'number',
label: 'Memory Discovery Max Dirs',
label: 'Discovery Max Dirs',
category: 'Context',
requiresRestart: false,
default: 200,
@@ -1149,7 +1167,7 @@ const SETTINGS_SCHEMA = {
},
loadMemoryFromIncludeDirectories: {
type: 'boolean',
label: 'Load Memory From Include Directories',
label: 'Memory From Include Directories',
category: 'Context',
requiresRestart: false,
default: false,
@@ -1170,7 +1188,7 @@ const SETTINGS_SCHEMA = {
properties: {
respectGitIgnore: {
type: 'boolean',
label: 'Respect .gitignore',
label: '.gitignore',
category: 'Context',
requiresRestart: true,
default: true,
@@ -1179,7 +1197,7 @@ const SETTINGS_SCHEMA = {
},
respectGeminiIgnore: {
type: 'boolean',
label: 'Respect .geminiignore',
label: '.geminiignore',
category: 'Context',
requiresRestart: true,
default: true,
@@ -1188,7 +1206,7 @@ const SETTINGS_SCHEMA = {
},
enableRecursiveFileSearch: {
type: 'boolean',
label: 'Enable Recursive File Search',
label: 'Recursive File Search',
category: 'Context',
requiresRestart: true,
default: true,
@@ -1199,7 +1217,7 @@ const SETTINGS_SCHEMA = {
},
enableFuzzySearch: {
type: 'boolean',
label: 'Enable Fuzzy Search',
label: 'Fuzzy Search',
category: 'Context',
requiresRestart: true,
default: true,
@@ -1256,7 +1274,7 @@ const SETTINGS_SCHEMA = {
properties: {
enableInteractiveShell: {
type: 'boolean',
label: 'Enable Interactive Shell',
label: 'Interactive Shell',
category: 'Tools',
requiresRestart: true,
default: true,
@@ -1278,7 +1296,7 @@ const SETTINGS_SCHEMA = {
},
showColor: {
type: 'boolean',
label: 'Show Color',
label: 'Color',
category: 'Tools',
requiresRestart: false,
default: false,
@@ -1297,7 +1315,7 @@ const SETTINGS_SCHEMA = {
},
enableShellOutputEfficiency: {
type: 'boolean',
label: 'Enable Shell Output Efficiency',
label: 'Shell Output Efficiency',
category: 'Tools',
requiresRestart: false,
default: true,
@@ -1369,7 +1387,7 @@ const SETTINGS_SCHEMA = {
},
useRipgrep: {
type: 'boolean',
label: 'Use Ripgrep',
label: 'Ripgrep',
category: 'Tools',
requiresRestart: false,
default: true,
@@ -1389,7 +1407,7 @@ const SETTINGS_SCHEMA = {
},
disableLLMCorrection: {
type: 'boolean',
label: 'Disable LLM Correction',
label: 'LLM Correction',
category: 'Tools',
requiresRestart: true,
default: true,
@@ -1399,6 +1417,24 @@ const SETTINGS_SCHEMA = {
`,
showInDialog: true,
},
approvalMode: {
type: 'enum',
label: 'Approval Mode',
category: 'Tools',
requiresRestart: false,
default: 'default',
description: oneLine`
The default approval mode for tool execution.
'default' prompts for approval, 'auto_edit' auto-approves edit tools,
and 'plan' is read-only mode. 'yolo' is not supported yet.
`,
showInDialog: true,
options: [
{ value: 'default', label: 'Default' },
{ value: 'auto_edit', label: 'Auto Edit' },
{ value: 'plan', label: 'Plan' },
],
},
},
},
@@ -1463,7 +1499,7 @@ const SETTINGS_SCHEMA = {
properties: {
disableYoloMode: {
type: 'boolean',
label: 'Disable YOLO Mode',
label: 'YOLO Mode',
category: 'Security',
requiresRestart: true,
default: false,
@@ -1472,7 +1508,7 @@ const SETTINGS_SCHEMA = {
},
enablePermanentToolApproval: {
type: 'boolean',
label: 'Allow Permanent Tool Approval',
label: 'Permanent Tool Approval',
category: 'Security',
requiresRestart: false,
default: false,
@@ -1482,7 +1518,7 @@ const SETTINGS_SCHEMA = {
},
blockGitExtensions: {
type: 'boolean',
label: 'Blocks extensions from Git',
label: 'Git Extensions',
category: 'Security',
requiresRestart: true,
default: false,
@@ -1552,7 +1588,7 @@ const SETTINGS_SCHEMA = {
},
enabled: {
type: 'boolean',
label: 'Enable Environment Variable Redaction',
label: 'Environment Variable Redaction',
category: 'Security',
requiresRestart: true,
default: false,
@@ -1592,7 +1628,7 @@ const SETTINGS_SCHEMA = {
},
useExternal: {
type: 'boolean',
label: 'Use External Auth',
label: 'External Auth',
category: 'Security',
requiresRestart: true,
default: undefined as boolean | undefined,
@@ -1687,7 +1723,7 @@ const SETTINGS_SCHEMA = {
properties: {
enabled: {
type: 'boolean',
label: 'Enable Tool Output Masking',
label: 'Tool Output Masking',
category: 'Experimental',
requiresRestart: true,
default: true,
@@ -1728,7 +1764,7 @@ const SETTINGS_SCHEMA = {
},
enableAgents: {
type: 'boolean',
label: 'Enable Agents',
label: 'Agents',
category: 'Experimental',
requiresRestart: true,
default: false,
@@ -1784,7 +1820,7 @@ const SETTINGS_SCHEMA = {
},
useOSC52Paste: {
type: 'boolean',
label: 'Use OSC 52 Paste',
label: 'OSC 52 Paste',
category: 'Experimental',
requiresRestart: false,
default: false,
@@ -1794,7 +1830,7 @@ const SETTINGS_SCHEMA = {
},
useOSC52Copy: {
type: 'boolean',
label: 'Use OSC 52 Copy',
label: 'OSC 52 Copy',
category: 'Experimental',
requiresRestart: false,
default: false,
@@ -1920,6 +1956,32 @@ const SETTINGS_SCHEMA = {
},
},
agents: {
type: 'object',
label: 'Agents',
category: 'Advanced',
requiresRestart: true,
default: {},
description: 'Settings for subagents.',
showInDialog: false,
properties: {
overrides: {
type: 'object',
label: 'Agent Overrides',
category: 'Advanced',
requiresRestart: true,
default: {} as Record<string, AgentOverride>,
description:
'Override settings for specific agents, e.g. to disable the agent, set a custom model config, or run config.',
showInDialog: false,
additionalProperties: {
type: 'object',
ref: 'AgentOverride',
},
},
},
},
skills: {
type: 'object',
label: 'Skills',
@@ -1931,7 +1993,7 @@ const SETTINGS_SCHEMA = {
properties: {
enabled: {
type: 'boolean',
label: 'Enable Agent Skills',
label: 'Agent Skills',
category: 'Advanced',
requiresRestart: true,
default: true,
@@ -1964,7 +2026,7 @@ const SETTINGS_SCHEMA = {
properties: {
enabled: {
type: 'boolean',
label: 'Enable Hooks',
label: 'Hooks',
category: 'Advanced',
requiresRestart: true,
default: true,
@@ -169,8 +169,8 @@ describe('Settings Repro', () => {
showCitations: true,
useInkScrolling: true,
footer: {
hideContextPercentage: false,
hideModelInfo: false,
contextPercentage: true,
modelInfo: true,
},
},
useWriteTodos: true,
+2 -2
View File
@@ -331,7 +331,7 @@ describe('gemini.tsx main function', () => {
});
describe('setWindowTitle', () => {
it('should set window title when hideWindowTitle is false', async () => {
it('should set window title when windowTitle is true', async () => {
// setWindowTitle is not exported, but we can test its effect if we had a way to call it.
// Since we can't easily call it directly without exporting it, we skip direct testing
// and rely on startInteractiveUI tests which call it.
@@ -1187,7 +1187,7 @@ describe('startInteractiveUI', () => {
const mockSettings = {
merged: {
ui: {
hideWindowTitle: false,
windowTitle: true,
useAlternateBuffer: true,
incrementalRendering: true,
},
+1 -1
View File
@@ -817,7 +817,7 @@ export async function main() {
}
function setWindowTitle(title: string, settings: LoadedSettings) {
if (!settings.merged.ui.hideWindowTitle) {
if (settings.merged.ui.windowTitle) {
// Initial state before React loop starts
const windowTitle = computeTerminalTitle({
streamingState: StreamingState.Idle,
+2 -2
View File
@@ -104,7 +104,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getExtensionLoader: vi.fn().mockReturnValue({}),
getEnabledExtensions: vi.fn().mockReturnValue([]),
getEnableExtensionReloading: vi.fn().mockReturnValue(false),
getDisableLLMCorrection: vi.fn().mockReturnValue(false),
getLLMCorrectionEnabled: vi.fn().mockReturnValue(false),
getNoBrowser: vi.fn().mockReturnValue(false),
getAgentsSettings: vi.fn().mockReturnValue({}),
getSummarizeToolOutputConfig: vi.fn().mockReturnValue(undefined),
@@ -123,7 +123,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
reloadAgents: vi.fn().mockResolvedValue(undefined),
getUseRipgrep: vi.fn().mockReturnValue(false),
getEnableInteractiveShell: vi.fn().mockReturnValue(false),
getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
getNextSpeakerCheckEnabled: vi.fn().mockReturnValue(false),
getContinueOnFailedApiCall: vi.fn().mockReturnValue(false),
getRetryFetchErrors: vi.fn().mockReturnValue(false),
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
+29 -29
View File
@@ -475,17 +475,17 @@ describe('AppContainer State Management', () => {
mockSettings = {
merged: {
...defaultMergedSettings,
hideBanner: false,
hideFooter: false,
hideTips: false,
showMemoryUsage: false,
theme: 'default',
ui: {
...defaultMergedSettings.ui,
banner: true,
footerEnabled: true,
tips: true,
showStatusInTitle: false,
hideWindowTitle: false,
windowTitle: true,
useAlternateBuffer: false,
},
showMemoryUsage: false,
theme: 'default',
},
} as unknown as LoadedSettings;
@@ -999,9 +999,9 @@ describe('AppContainer State Management', () => {
const settingsAllHidden = {
merged: {
...defaultMergedSettings,
hideBanner: true,
hideFooter: true,
hideTips: true,
banner: false,
footerEnabled: false,
tips: false,
showMemoryUsage: false,
},
} as unknown as LoadedSettings;
@@ -1020,9 +1020,9 @@ describe('AppContainer State Management', () => {
const settingsWithMemory = {
merged: {
...defaultMergedSettings,
hideBanner: false,
hideFooter: false,
hideTips: false,
banner: true,
footerEnabled: true,
tips: true,
showMemoryUsage: true,
},
} as unknown as LoadedSettings;
@@ -1493,7 +1493,7 @@ describe('AppContainer State Management', () => {
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: false,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -1531,7 +1531,7 @@ describe('AppContainer State Management', () => {
ui: {
...mockSettings.merged.ui,
dynamicWindowTitle: false,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -1560,24 +1560,24 @@ describe('AppContainer State Management', () => {
unmount();
});
it('should not update terminal title when hideWindowTitle is true', () => {
// Arrange: Set up mock settings with hideWindowTitle enabled
it('should not update terminal title when windowTitle is false', () => {
// Arrange: Set up mock settings with windowTitle disabled
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const mockSettingsWithHideTitleTrue = {
const mockSettingsWithTitleFalse = {
...mockSettings,
merged: {
...defaultMergedSettings,
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: true,
windowTitle: false,
},
},
} as unknown as LoadedSettings;
// Act: Render the container
const { unmount } = renderAppContainer({
settings: mockSettingsWithHideTitleTrue,
settings: mockSettingsWithTitleFalse,
});
// Assert: Check that no title-related writes occurred
@@ -1599,7 +1599,7 @@ describe('AppContainer State Management', () => {
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -1639,7 +1639,7 @@ describe('AppContainer State Management', () => {
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -1674,7 +1674,7 @@ describe('AppContainer State Management', () => {
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -1736,7 +1736,7 @@ describe('AppContainer State Management', () => {
ui: {
...mockSettings.merged.ui,
showStatusInTitle: true,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -1795,7 +1795,7 @@ describe('AppContainer State Management', () => {
ui: {
...mockSettings.merged.ui,
showStatusInTitle: true,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -1865,7 +1865,7 @@ describe('AppContainer State Management', () => {
ui: {
...mockSettings.merged.ui,
showStatusInTitle: true,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -1915,7 +1915,7 @@ describe('AppContainer State Management', () => {
ui: {
...mockSettings.merged.ui,
showStatusInTitle: true,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -2000,7 +2000,7 @@ describe('AppContainer State Management', () => {
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -2041,7 +2041,7 @@ describe('AppContainer State Management', () => {
ui: {
...defaultMergedSettings.ui,
showStatusInTitle: true,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
@@ -2079,7 +2079,7 @@ describe('AppContainer State Management', () => {
ui: {
...mockSettings.merged.ui,
showStatusInTitle: false,
hideWindowTitle: false,
windowTitle: true,
},
},
} as unknown as LoadedSettings;
+5 -4
View File
@@ -640,7 +640,8 @@ export const AppContainer = (props: AppContainerProps) => {
useEffect(() => {
if (
!(settings.merged.ui.hideBanner || config.getScreenReader()) &&
settings.merged.ui.banner &&
!config.getScreenReader() &&
bannerVisible &&
bannerText
) {
@@ -1916,8 +1917,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
);
useEffect(() => {
// Respect hideWindowTitle settings
if (settings.merged.ui.hideWindowTitle) return;
// Respect windowTitle settings
if (!settings.merged.ui.windowTitle) return;
const paddedTitle = computeTerminalTitle({
streamingState,
@@ -1944,7 +1945,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
shouldShowSilentWorkingTitle,
settings.merged.ui.showStatusInTitle,
settings.merged.ui.dynamicWindowTitle,
settings.merged.ui.hideWindowTitle,
settings.merged.ui.windowTitle,
config,
stdout,
]);
+4 -3
View File
@@ -38,7 +38,7 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
return (
<Box flexDirection="column">
{!(settings.merged.ui.hideBanner || config.getScreenReader()) && (
{settings.merged.ui.banner && !config.getScreenReader() && (
<>
<Header version={version} nightly={nightly} />
{bannerVisible && bannerText && (
@@ -53,8 +53,9 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
{settings.merged.ui.showUserIdentity !== false && (
<UserIdentity config={config} />
)}
{!(settings.merged.ui.hideTips || config.getScreenReader()) &&
showTips && <Tips config={config} />}
{settings.merged.ui.tips && !config.getScreenReader() && showTips && (
<Tips config={config} />
)}
</Box>
);
};
@@ -288,18 +288,18 @@ describe('Composer', () => {
});
describe('Footer Display Settings', () => {
it('renders Footer by default when hideFooter is false', async () => {
it('renders Footer by default when footerEnabled is true', async () => {
const uiState = createMockUIState();
const settings = createMockSettings({ ui: { hideFooter: false } });
const settings = createMockSettings({ ui: { footerEnabled: true } });
const { lastFrame } = await renderComposer(uiState, settings);
expect(lastFrame()).toContain('Footer');
});
it('does NOT render Footer when hideFooter is true', async () => {
it('does NOT render Footer when footerEnabled is false', async () => {
const uiState = createMockUIState();
const settings = createMockSettings({ ui: { hideFooter: true } });
const settings = createMockSettings({ ui: { footerEnabled: false } });
const { lastFrame } = await renderComposer(uiState, settings);
@@ -332,7 +332,7 @@ describe('Composer', () => {
});
const settings = createMockSettings({
ui: {
hideFooter: false,
footerEnabled: true,
showMemoryUsage: true,
},
});
@@ -744,7 +744,7 @@ describe('Composer', () => {
});
const settings = createMockSettings({
ui: {
footer: { hideContextPercentage: false },
footer: { contextPercentage: true },
},
});
+7 -5
View File
@@ -58,7 +58,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const { showApprovalModeIndicator } = uiState;
const showUiDetails = uiState.cleanUiDetailsVisible;
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
const hideContextSummary =
const forceHideContextSummary =
suggestionsVisible && suggestionsPosition === 'above';
const hasPendingToolConfirmation = useMemo(
@@ -143,7 +143,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const hasMinimalStatusBleedThrough = shouldShowToast(uiState);
const showMinimalContextBleedThrough =
!settings.merged.ui.footer.hideContextPercentage &&
settings.merged.ui.footer.contextPercentage &&
isContextUsageHigh(
uiState.sessionStats.lastPromptTokenCount,
typeof uiState.currentModel === 'string'
@@ -338,7 +338,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
{showUiDetails && (
<Box
justifyContent={
settings.merged.ui.hideContextSummary
!settings.merged.ui.contextSummary
? 'flex-start'
: 'space-between'
}
@@ -410,7 +410,9 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
>
{!showLoadingIndicator && (
<StatusDisplay hideContextSummary={hideContextSummary} />
<StatusDisplay
forceHideContextSummary={forceHideContextSummary}
/>
)}
</Box>
</Box>
@@ -470,7 +472,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
)}
{showUiDetails &&
!settings.merged.ui.hideFooter &&
settings.merged.ui.footerEnabled &&
!isScreenReaderEnabled && <Footer />}
</Box>
);
+17 -17
View File
@@ -153,7 +153,7 @@ describe('<Footer />', () => {
settings: createMockSettings({
ui: {
footer: {
hideContextPercentage: false,
contextPercentage: true,
},
},
}),
@@ -258,7 +258,7 @@ describe('<Footer />', () => {
settings: createMockSettings({
ui: {
footer: {
hideContextPercentage: false,
contextPercentage: true,
},
},
}),
@@ -370,7 +370,7 @@ describe('<Footer />', () => {
settings: createMockSettings({
ui: {
footer: {
hideContextPercentage: false,
contextPercentage: true,
},
},
}),
@@ -390,9 +390,9 @@ describe('<Footer />', () => {
settings: createMockSettings({
ui: {
footer: {
hideCWD: true,
hideSandboxStatus: true,
hideModelInfo: true,
cwd: false,
sandboxStatus: false,
modelInfo: false,
},
},
}),
@@ -412,9 +412,9 @@ describe('<Footer />', () => {
settings: createMockSettings({
ui: {
footer: {
hideCWD: false,
hideSandboxStatus: false,
hideModelInfo: true,
cwd: true,
sandboxStatus: true,
modelInfo: false,
},
},
}),
@@ -434,9 +434,9 @@ describe('<Footer />', () => {
settings: createMockSettings({
ui: {
footer: {
hideCWD: true,
hideSandboxStatus: false,
hideModelInfo: true,
cwd: false,
sandboxStatus: true,
modelInfo: false,
},
},
}),
@@ -447,7 +447,7 @@ describe('<Footer />', () => {
unmount();
});
it('hides the context percentage when hideContextPercentage is true', async () => {
it('hides the context percentage when contextPercentage is false', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
@@ -456,7 +456,7 @@ describe('<Footer />', () => {
settings: createMockSettings({
ui: {
footer: {
hideContextPercentage: true,
contextPercentage: false,
},
},
}),
@@ -467,7 +467,7 @@ describe('<Footer />', () => {
expect(lastFrame()).not.toMatch(/\d+% context left/);
unmount();
});
it('shows the context percentage when hideContextPercentage is false', async () => {
it('shows the context percentage when contextPercentage is true', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<Footer />,
{
@@ -476,7 +476,7 @@ describe('<Footer />', () => {
settings: createMockSettings({
ui: {
footer: {
hideContextPercentage: false,
contextPercentage: true,
},
},
}),
@@ -496,7 +496,7 @@ describe('<Footer />', () => {
settings: createMockSettings({
ui: {
footer: {
hideContextPercentage: false,
contextPercentage: true,
},
},
}),
+12 -10
View File
@@ -63,15 +63,17 @@ export const Footer: React.FC = () => {
const isFullErrorVerbosity = settings.merged.ui.errorVerbosity === 'full';
const showErrorSummary =
!showErrorDetails && errorCount > 0 && (isFullErrorVerbosity || debugMode);
const hideCWD = settings.merged.ui.footer.hideCWD;
const hideSandboxStatus = settings.merged.ui.footer.hideSandboxStatus;
const hideModelInfo = settings.merged.ui.footer.hideModelInfo;
const hideContextPercentage = settings.merged.ui.footer.hideContextPercentage;
const showCWD = !settings.merged.ui.footer.hideCWD;
const showSandboxStatus = !settings.merged.ui.footer.hideSandboxStatus;
const showModelInfo = !settings.merged.ui.footer.hideModelInfo;
const showContextPercentage = !settings.merged.ui.footer.hideContextPercentage;
const pathLength = Math.max(20, Math.floor(terminalWidth * 0.25));
const displayPath = shortenPath(tildeifyPath(targetDir), pathLength);
const justifyContent = hideCWD && hideModelInfo ? 'center' : 'space-between';
const justifyContent =
!showCWD && !showModelInfo ? 'center' : 'space-between';
const displayVimMode = vimEnabled ? vimMode : undefined;
const showDebugProfiler = debugMode || isDevelopment;
@@ -84,13 +86,13 @@ export const Footer: React.FC = () => {
alignItems="center"
paddingX={1}
>
{(showDebugProfiler || displayVimMode || !hideCWD) && (
{(showDebugProfiler || displayVimMode || showCWD) && (
<Box>
{showDebugProfiler && <DebugProfiler />}
{displayVimMode && (
<Text color={theme.text.secondary}>[{displayVimMode}] </Text>
)}
{!hideCWD && (
{showCWD && (
<Text color={theme.text.primary}>
{displayPath}
{branchName && (
@@ -107,7 +109,7 @@ export const Footer: React.FC = () => {
)}
{/* Middle Section: Centered Trust/Sandbox Info */}
{!hideSandboxStatus && (
{showSandboxStatus && (
<Box
flexGrow={1}
alignItems="center"
@@ -140,13 +142,13 @@ export const Footer: React.FC = () => {
)}
{/* Right Section: Gemini Label and Console Summary */}
{!hideModelInfo && (
{showModelInfo && (
<Box alignItems="center" justifyContent="flex-end">
<Box alignItems="center">
<Text color={theme.text.primary}>
<Text color={theme.text.secondary}>/model </Text>
{getDisplayString(model)}
{!hideContextPercentage && (
{showContextPercentage && (
<>
{' '}
<ContextUsageDisplay
@@ -313,7 +313,7 @@ describe('SettingsDialog', () => {
expect(output).toContain('Vim Mode');
expect(output).toContain('Enable Vim keybindings');
// 'general.enableAutoUpdate' has description 'Enable automatic updates.'
expect(output).toContain('Enable Auto Update');
expect(output).toContain('Auto Update');
expect(output).toContain('Enable automatic updates.');
unmount();
});
@@ -351,7 +351,7 @@ describe('SettingsDialog', () => {
await waitUntilReady();
await waitFor(() => {
expect(lastFrame()).toContain('Enable Auto Update');
expect(lastFrame()).toContain('Auto Update');
});
// Navigate up
@@ -796,13 +796,13 @@ describe('SettingsDialog', () => {
it('should show correct display values for settings with different states', async () => {
const settings = createMockSettings({
user: {
settings: { vimMode: true, hideTips: false },
originalSettings: { vimMode: true, hideTips: false },
settings: { vimMode: true, tips: true },
originalSettings: { vimMode: true, tips: true },
path: '',
},
system: {
settings: { hideWindowTitle: true },
originalSettings: { hideWindowTitle: true },
settings: { windowTitle: false },
originalSettings: { windowTitle: false },
path: '',
},
workspace: {
@@ -835,7 +835,7 @@ describe('SettingsDialog', () => {
);
await waitUntilReady();
// Toggle a non-restart-required setting (like hideTips)
// Toggle a non-restart-required setting (like tips)
await act(async () => {
stdin.write(TerminalKeys.ENTER as string); // Enter - toggle current setting
});
@@ -884,8 +884,8 @@ describe('SettingsDialog', () => {
it('should show correct values for inherited settings', async () => {
const settings = createMockSettings({
system: {
settings: { vimMode: true, hideWindowTitle: false },
originalSettings: { vimMode: true, hideWindowTitle: false },
settings: { vimMode: true, windowTitle: true },
originalSettings: { vimMode: true, windowTitle: true },
path: '',
},
});
@@ -1495,7 +1495,7 @@ describe('SettingsDialog', () => {
await waitFor(() => {
expect(lastFrame()).toContain('yolo');
expect(lastFrame()).toContain('Disable YOLO Mode');
expect(lastFrame()).toContain('YOLO Mode');
});
unmount();
@@ -1586,7 +1586,7 @@ describe('SettingsDialog', () => {
await waitFor(() => {
expect(lastFrame()).toContain('nonexistentsetting');
expect(lastFrame()).not.toContain('Vim Mode'); // Should not contain any settings
expect(lastFrame()).not.toContain('Enable Auto Update'); // Should not contain any settings
expect(lastFrame()).not.toContain('Auto Update'); // Should not contain any settings
});
unmount();
@@ -1617,8 +1617,8 @@ describe('SettingsDialog', () => {
debugKeystrokeLogging: true,
},
ui: {
hideWindowTitle: true,
hideTips: true,
windowTitle: false,
tips: false,
showMemoryUsage: true,
showLineNumbers: true,
showCitations: true,
@@ -1662,7 +1662,7 @@ describe('SettingsDialog', () => {
},
ui: {
showMemoryUsage: true,
hideWindowTitle: false,
windowTitle: true,
},
tools: {
truncateToolOutputThreshold: 50000,
@@ -1672,7 +1672,7 @@ describe('SettingsDialog', () => {
},
model: {
maxSessionTurns: 100,
skipNextSpeakerCheck: false,
nextSpeakerCheck: true,
},
},
systemSettings: {},
@@ -1746,7 +1746,7 @@ describe('SettingsDialog', () => {
},
model: {
maxSessionTurns: 50,
skipNextSpeakerCheck: true,
nextSpeakerCheck: false,
},
},
systemSettings: {},
@@ -1762,8 +1762,8 @@ describe('SettingsDialog', () => {
debugKeystrokeLogging: false,
},
ui: {
hideWindowTitle: false,
hideTips: false,
windowTitle: true,
tips: true,
showMemoryUsage: false,
showLineNumbers: false,
showCitations: false,
@@ -249,6 +249,7 @@ export function SettingsDialog({
return {
key,
label: definition?.label || key,
category: definition?.category,
description: definition?.description,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
type: type as 'boolean' | 'number' | 'string' | 'enum',
@@ -621,6 +622,7 @@ export function SettingsDialog({
const HELP_TEXT_HEIGHT = 1;
const RESTART_PROMPT_HEIGHT = showRestartPrompt ? 1 : 0;
const ITEM_HEIGHT = 3; // Label + description + spacing
const HEADER_HEIGHT = 2; // Category Label + spacing
const currentAvailableHeight = availableTerminalHeight - DIALOG_PADDING;
@@ -632,13 +634,18 @@ export function SettingsDialog({
HELP_TEXT_HEIGHT +
RESTART_PROMPT_HEIGHT;
// Estimate average number of items per category to account for headers
// In the default schema, we have about 10 categories for ~30 settings shown in dialog.
// So roughly 1 header per 3 items.
const EFFECTIVE_ITEM_HEIGHT = ITEM_HEIGHT + HEADER_HEIGHT / 3;
// Calculate max items with scope selector
const heightWithScope = baseFixedHeight + SCOPE_SECTION_HEIGHT;
const availableForItemsWithScope =
currentAvailableHeight - heightWithScope;
const maxItemsWithScope = Math.max(
1,
Math.floor(availableForItemsWithScope / ITEM_HEIGHT),
Math.floor(availableForItemsWithScope / EFFECTIVE_ITEM_HEIGHT),
);
// Calculate max items without scope selector
@@ -646,7 +653,7 @@ export function SettingsDialog({
currentAvailableHeight - baseFixedHeight;
const maxItemsWithoutScope = Math.max(
1,
Math.floor(availableForItemsWithoutScope / ITEM_HEIGHT),
Math.floor(availableForItemsWithoutScope / EFFECTIVE_ITEM_HEIGHT),
);
// In small terminals, hide scope selector if it would allow more items to show
@@ -70,7 +70,9 @@ const createMockConfig = (overrides = {}) => ({
});
const renderStatusDisplay = async (
props: { hideContextSummary: boolean } = { hideContextSummary: false },
props: { forceHideContextSummary: boolean } = {
forceHideContextSummary: false,
},
uiState: UIState = createMockUIState(),
settings = createMockSettings(),
config = createMockConfig(),
@@ -99,7 +101,7 @@ describe('StatusDisplay', () => {
it('renders nothing by default if context summary is hidden via props', async () => {
const { lastFrame, unmount } = await renderStatusDisplay({
hideContextSummary: true,
forceHideContextSummary: true,
});
expect(lastFrame({ allowEmpty: true })).toBe('');
unmount();
@@ -123,7 +125,7 @@ describe('StatusDisplay', () => {
activeHooks: [{ name: 'hook', eventName: 'event' }],
});
const { lastFrame, unmount } = await renderStatusDisplay(
{ hideContextSummary: false },
{ forceHideContextSummary: false },
uiState,
);
expect(lastFrame()).toMatchSnapshot();
@@ -138,7 +140,7 @@ describe('StatusDisplay', () => {
hooksConfig: { notifications: false },
});
const { lastFrame, unmount } = await renderStatusDisplay(
{ hideContextSummary: false },
{ forceHideContextSummary: false },
uiState,
settings,
);
@@ -146,12 +148,12 @@ describe('StatusDisplay', () => {
unmount();
});
it('hides ContextSummaryDisplay if configured in settings', async () => {
it('hides ContextSummaryDisplay if disabled in settings', async () => {
const settings = createMockSettings({
ui: { hideContextSummary: true },
ui: { contextSummary: false },
});
const { lastFrame, unmount } = await renderStatusDisplay(
{ hideContextSummary: false },
{ forceHideContextSummary: false },
undefined,
settings,
);
@@ -164,7 +166,7 @@ describe('StatusDisplay', () => {
backgroundShellCount: 3,
});
const { lastFrame, unmount } = await renderStatusDisplay(
{ hideContextSummary: false },
{ forceHideContextSummary: false },
uiState,
);
expect(lastFrame()).toContain('Shells: 3');
@@ -14,11 +14,11 @@ import { ContextSummaryDisplay } from './ContextSummaryDisplay.js';
import { HookStatusDisplay } from './HookStatusDisplay.js';
interface StatusDisplayProps {
hideContextSummary: boolean;
forceHideContextSummary: boolean;
}
export const StatusDisplay: React.FC<StatusDisplayProps> = ({
hideContextSummary,
forceHideContextSummary,
}) => {
const uiState = useUIState();
const settings = useSettings();
@@ -35,7 +35,10 @@ export const StatusDisplay: React.FC<StatusDisplayProps> = ({
return <HookStatusDisplay activeHooks={uiState.activeHooks} />;
}
if (!settings.merged.ui.hideContextSummary && !hideContextSummary) {
const showContextSummary =
settings.merged.ui.contextSummary && !forceHideContextSummary;
if (showContextSummary) {
return (
<ContextSummaryDisplay
ideContext={uiState.ideContextState}
@@ -30,6 +30,8 @@ export interface SettingsDialogItem {
key: string;
/** Display label */
label: string;
/** Optional category for grouping */
category?: string;
/** Optional description below label */
description?: string;
/** Item type for determining interaction behavior */
@@ -485,6 +487,11 @@ export function BaseSettingsDialog({
const isActive =
focusSection === 'settings' && activeIndex === globalIndex;
const previousItem =
globalIndex > 0 ? items[globalIndex - 1] : undefined;
const showCategoryHeader =
item.category && item.category !== previousItem?.category;
// Compute display value with edit mode cursor
let displayValue: string;
if (editingKey === item.key) {
@@ -514,6 +521,27 @@ export function BaseSettingsDialog({
return (
<React.Fragment key={item.key}>
{showCategoryHeader && (
<Box
marginX={1}
marginBottom={1}
marginTop={idx === 0 ? 0 : 1}
flexDirection="row"
alignItems="center"
height={1}
>
<Text bold>{item.category} </Text>
<Box
flexGrow={1}
borderStyle="single"
borderTop
borderBottom={false}
borderLeft={false}
borderRight={false}
borderColor={theme.border.default}
/>
</Box>
)}
<Box marginX={1} flexDirection="row" alignItems="flex-start">
<Box minWidth={2} flexShrink={0}>
<Text
+12 -15
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
// Schema utilities
getSettingsByCategory,
@@ -381,7 +381,7 @@ describe('SettingsUtils', () => {
it('should return true for settings marked to show in dialog', () => {
expect(shouldShowInDialog('ui.requiresRestart')).toBe(true);
expect(shouldShowInDialog('general.vimMode')).toBe(true);
expect(shouldShowInDialog('ui.hideWindowTitle')).toBe(true);
expect(shouldShowInDialog('ui.windowTitle')).toBe(true);
});
it('should return false for settings marked to hide from dialog', () => {
@@ -553,7 +553,7 @@ describe('SettingsUtils', () => {
new Set(),
updatedPendingSettings,
);
expect(displayValue).toBe('true'); // Should show true (no * since value matches default)
expect(displayValue).toBe('On'); // Should show On (no * since value matches default)
// Test that modified settings also show the * indicator
const modifiedSettings = new Set([key]);
@@ -564,7 +564,7 @@ describe('SettingsUtils', () => {
modifiedSettings,
{},
);
expect(displayValueWithModified).toBe('true*'); // Should show true* because it's in modified settings and default is true
expect(displayValueWithModified).toBe('On*'); // Should show On* because it's in modified settings and default is true
});
});
});
@@ -678,12 +678,12 @@ describe('SettingsUtils', () => {
it('should set top-level setting value', () => {
const pendingSettings = makeMockSettings({});
const result = setPendingSettingValue(
'ui.hideWindowTitle',
'ui.windowTitle',
true,
pendingSettings,
);
expect(result.ui?.hideWindowTitle).toBe(true);
expect(result.ui?.windowTitle).toBe(true);
});
it('should set nested setting value', () => {
@@ -751,10 +751,7 @@ describe('SettingsUtils', () => {
});
it('should return empty array when no settings require restart', () => {
const modifiedSettings = new Set<string>([
'requiresRestart',
'hideTips',
]);
const modifiedSettings = new Set<string>(['requiresRestart', 'tips']);
const result = getRestartRequiredFromModified(modifiedSettings);
expect(result).toEqual([]);
@@ -953,7 +950,7 @@ describe('SettingsUtils', () => {
mergedSettings,
modifiedSettings,
);
expect(result).toBe('false*');
expect(result).toBe('Off*');
});
it('should show default value when setting is not in scope', () => {
@@ -969,7 +966,7 @@ describe('SettingsUtils', () => {
mergedSettings,
modifiedSettings,
);
expect(result).toBe('false'); // shows default value
expect(result).toBe('Off'); // shows default value
});
it('should show value with * when changed from default', () => {
@@ -985,7 +982,7 @@ describe('SettingsUtils', () => {
mergedSettings,
modifiedSettings,
);
expect(result).toBe('true*');
expect(result).toBe('On*');
});
it('should show default value without * when setting does not exist in scope', () => {
@@ -1001,7 +998,7 @@ describe('SettingsUtils', () => {
mergedSettings,
modifiedSettings,
);
expect(result).toBe('false'); // default value (false) without *
expect(result).toBe('Off'); // default value (false) without *
});
it('should show value with * when user changes from default', () => {
@@ -1021,7 +1018,7 @@ describe('SettingsUtils', () => {
modifiedSettings,
pendingSettings,
);
expect(result).toBe('true*'); // changed from default (false) to true
expect(result).toBe('On*'); // changed from default (false) to true
});
});
+3 -1
View File
@@ -470,7 +470,9 @@ export function getDisplayValue(
let valueString = String(value);
if (definition?.type === 'enum' && definition.options) {
if (definition?.type === 'boolean') {
valueString = value === true ? 'On' : 'Off';
} else if (definition?.type === 'enum' && definition.options) {
const option = definition.options?.find((option) => option.value === value);
valueString = option?.label ?? `${value}`;
}
+29 -3
View File
@@ -72,7 +72,9 @@ vi.mock('../tools/tool-registry', () => {
const ToolRegistryMock = vi.fn();
ToolRegistryMock.prototype.registerTool = vi.fn();
ToolRegistryMock.prototype.unregisterTool = vi.fn();
ToolRegistryMock.prototype.discoverAllTools = vi.fn();
ToolRegistryMock.prototype.discoverAllTools = vi
.fn()
.mockResolvedValue(undefined);
ToolRegistryMock.prototype.sortTools = vi.fn();
ToolRegistryMock.prototype.getAllTools = vi.fn(() => []); // Mock methods if needed
ToolRegistryMock.prototype.getTool = vi.fn();
@@ -91,6 +93,13 @@ vi.mock('../utils/memoryDiscovery.js', () => ({
loadServerHierarchicalMemory: vi.fn(),
}));
vi.mock('../utils/extensionLoader.js', () => ({
SimpleExtensionLoader: vi.fn().mockImplementation(() => ({
start: vi.fn().mockResolvedValue(undefined),
getExtensions: vi.fn().mockReturnValue([]),
})),
}));
// Mock individual tools if their constructors are complex or have side effects
vi.mock('../tools/ls');
vi.mock('../tools/read-file');
@@ -113,7 +122,18 @@ vi.mock('../tools/memoryTool', () => ({
GEMINI_DIR: '.gemini',
}));
vi.mock('../core/contentGenerator.js');
vi.mock('../core/contentGenerator.js', () => ({
createContentGenerator: vi.fn().mockResolvedValue({
userTier: 'test-tier',
userTierName: 'test-tier-name',
}),
createContentGeneratorConfig: vi.fn().mockResolvedValue({}),
AuthType: {
USE_GEMINI: 'USE_GEMINI',
USE_VERTEX_AI: 'USE_VERTEX_AI',
LOGIN_WITH_GOOGLE: 'LOGIN_WITH_GOOGLE',
},
}));
vi.mock('../core/client.js', () => ({
GeminiClient: vi.fn().mockImplementation(() => ({
@@ -166,7 +186,7 @@ vi.mock('../ide/ide-client.js', () => ({
vi.mock('../agents/registry.js', () => {
const AgentRegistryMock = vi.fn();
AgentRegistryMock.prototype.initialize = vi.fn();
AgentRegistryMock.prototype.initialize = vi.fn().mockResolvedValue(undefined);
AgentRegistryMock.prototype.getAllDefinitions = vi.fn(() => []);
AgentRegistryMock.prototype.getDefinition = vi.fn();
return { AgentRegistry: AgentRegistryMock };
@@ -1519,6 +1539,9 @@ describe('GemmaModelRouterSettings', () => {
});
describe('setApprovalMode with folder trust', () => {
beforeEach(() => {
vi.spyOn(Storage.prototype, 'getPlansDir').mockReturnValue('/tmp/plans');
});
const baseParams: ConfigParameters = {
sessionId: 'test',
targetDir: '.',
@@ -2968,6 +2991,9 @@ describe('Plans Directory Initialization', () => {
});
describe('syncPlanModeTools', () => {
beforeEach(() => {
vi.spyOn(Storage.prototype, 'getPlansDir').mockReturnValue('/tmp/plans');
});
const baseParams: ConfigParameters = {
sessionId: 'test-session',
targetDir: '.',
+1 -1
View File
@@ -1366,7 +1366,7 @@ export class Config implements McpContext {
}
getDisableLoopDetection(): boolean {
return this.disableLoopDetection ?? false;
return this.disableLoopDetection;
}
setModel(newModel: string, isTemporary: boolean = true): void {
+119 -119
View File
@@ -53,7 +53,7 @@
"type": "boolean"
},
"defaultApprovalMode": {
"title": "Default Approval Mode",
"title": "Approval Mode",
"description": "The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet.",
"markdownDescription": "The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `default`",
"default": "default",
@@ -68,21 +68,21 @@
"type": "boolean"
},
"enableAutoUpdate": {
"title": "Enable Auto Update",
"title": "Auto Update",
"description": "Enable automatic updates.",
"markdownDescription": "Enable automatic updates.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"enableAutoUpdateNotification": {
"title": "Enable Auto Update Notification",
"title": "Auto Update Notification",
"description": "Enable update notification prompts.",
"markdownDescription": "Enable update notification prompts.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"enableNotifications": {
"title": "Enable Notifications",
"title": "Notifications",
"description": "Enable run-event notifications for action-required prompts and session completion. Currently macOS only.",
"markdownDescription": "Enable run-event notifications for action-required prompts and session completion. Currently macOS only.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
@@ -96,7 +96,7 @@
"type": "object",
"properties": {
"enabled": {
"title": "Enable Checkpointing",
"title": "Checkpointing",
"description": "Enable session checkpointing for recovery",
"markdownDescription": "Enable session checkpointing for recovery\n\n- Category: `General`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
@@ -156,14 +156,14 @@
"type": "object",
"properties": {
"enabled": {
"title": "Enable Session Cleanup",
"title": "Session Cleanup",
"description": "Enable automatic session cleanup",
"markdownDescription": "Enable automatic session cleanup\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"maxAge": {
"title": "Keep chat history",
"title": "Chat History Period",
"description": "Automatically delete chats older than this time period (e.g., \"30d\", \"7d\", \"24h\", \"1w\")",
"markdownDescription": "Automatically delete chats older than this time period (e.g., \"30d\", \"7d\", \"24h\", \"1w\")\n\n- Category: `General`\n- Requires restart: `no`",
"type": "string"
@@ -249,11 +249,11 @@
"$ref": "#/$defs/CustomTheme"
}
},
"hideWindowTitle": {
"title": "Hide Window Title",
"description": "Hide the window title bar",
"markdownDescription": "Hide the window title bar\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"windowTitle": {
"title": "Window Title",
"description": "Show the window title bar",
"markdownDescription": "Show the window title bar\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"inlineThinkingMode": {
@@ -265,7 +265,7 @@
"enum": ["off", "full"]
},
"showStatusInTitle": {
"title": "Show Thoughts in Title",
"title": "Thoughts in Title",
"description": "Show Gemini CLI model thoughts in the terminal window title during the working phase",
"markdownDescription": "Show Gemini CLI model thoughts in the terminal window title during the working phase\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
@@ -279,45 +279,45 @@
"type": "boolean"
},
"showHomeDirectoryWarning": {
"title": "Show Home Directory Warning",
"title": "Home Directory Warning",
"description": "Show a warning when running Gemini CLI in the home directory.",
"markdownDescription": "Show a warning when running Gemini CLI in the home directory.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"showCompatibilityWarnings": {
"title": "Show Compatibility Warnings",
"title": "Compatibility Warnings",
"description": "Show warnings about terminal or OS compatibility issues.",
"markdownDescription": "Show warnings about terminal or OS compatibility issues.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"hideTips": {
"title": "Hide Tips",
"description": "Hide helpful tips in the UI",
"markdownDescription": "Hide helpful tips in the UI\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"tips": {
"title": "Tips",
"description": "Show helpful tips in the UI",
"markdownDescription": "Show helpful tips in the UI\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"showShortcutsHint": {
"title": "Show Shortcuts Hint",
"title": "Shortcuts Hint",
"description": "Show the \"? for shortcuts\" hint above the input.",
"markdownDescription": "Show the \"? for shortcuts\" hint above the input.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"hideBanner": {
"title": "Hide Banner",
"description": "Hide the application banner",
"markdownDescription": "Hide the application banner\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"banner": {
"title": "Banner",
"description": "Show the application banner",
"markdownDescription": "Show the application banner\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"hideContextSummary": {
"title": "Hide Context Summary",
"description": "Hide the context summary (GEMINI.md, MCP servers) above the input.",
"markdownDescription": "Hide the context summary (GEMINI.md, MCP servers) above the input.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"contextSummary": {
"title": "Context Summary",
"description": "Show the context summary (GEMINI.md, MCP servers) above the input.",
"markdownDescription": "Show the context summary (GEMINI.md, MCP servers) above the input.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"footer": {
@@ -327,88 +327,88 @@
"default": {},
"type": "object",
"properties": {
"hideCWD": {
"title": "Hide CWD",
"description": "Hide the current working directory path in the footer.",
"markdownDescription": "Hide the current working directory path in the footer.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"hideSandboxStatus": {
"title": "Hide Sandbox Status",
"description": "Hide the sandbox status indicator in the footer.",
"markdownDescription": "Hide the sandbox status indicator in the footer.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"hideModelInfo": {
"title": "Hide Model Info",
"description": "Hide the model name and context usage in the footer.",
"markdownDescription": "Hide the model name and context usage in the footer.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"hideContextPercentage": {
"title": "Hide Context Window Percentage",
"description": "Hides the context window remaining percentage.",
"markdownDescription": "Hides the context window remaining percentage.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"cwd": {
"title": "CWD",
"description": "Show the current working directory path in the footer.",
"markdownDescription": "Show the current working directory path in the footer.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"sandboxStatus": {
"title": "Sandbox Status",
"description": "Show the sandbox status indicator in the footer.",
"markdownDescription": "Show the sandbox status indicator in the footer.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"modelInfo": {
"title": "Model Info",
"description": "Show the model name and context usage in the footer.",
"markdownDescription": "Show the model name and context usage in the footer.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"contextPercentage": {
"title": "Context Window Percentage",
"description": "Shows the context window remaining percentage.",
"markdownDescription": "Shows the context window remaining percentage.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
}
},
"additionalProperties": false
},
"hideFooter": {
"title": "Hide Footer",
"description": "Hide the footer from the UI",
"markdownDescription": "Hide the footer from the UI\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"footerEnabled": {
"title": "Footer",
"description": "Show the footer in the UI",
"markdownDescription": "Show the footer in the UI\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"showMemoryUsage": {
"title": "Show Memory Usage",
"title": "Memory Usage",
"description": "Display memory usage information in the UI",
"markdownDescription": "Display memory usage information in the UI\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"showLineNumbers": {
"title": "Show Line Numbers",
"title": "Line Numbers",
"description": "Show line numbers in the chat.",
"markdownDescription": "Show line numbers in the chat.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"showCitations": {
"title": "Show Citations",
"title": "Citations",
"description": "Show citations for generated text in the chat.",
"markdownDescription": "Show citations for generated text in the chat.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"showModelInfoInChat": {
"title": "Show Model Info In Chat",
"title": "Model Info In Chat",
"description": "Show the model name in the chat for each model turn.",
"markdownDescription": "Show the model name in the chat for each model turn.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"showUserIdentity": {
"title": "Show User Identity",
"title": "User Identity",
"description": "Show the logged-in user's identity (e.g. email) in the UI.",
"markdownDescription": "Show the logged-in user's identity (e.g. email) in the UI.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"useAlternateBuffer": {
"title": "Use Alternate Screen Buffer",
"title": "Alternate Screen Buffer",
"description": "Use an alternate screen buffer for the UI, preserving shell history.",
"markdownDescription": "Use an alternate screen buffer for the UI, preserving shell history.\n\n- Category: `UI`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"useBackgroundColor": {
"title": "Use Background Color",
"title": "Background Color",
"description": "Whether to use background colors in the UI.",
"markdownDescription": "Whether to use background colors in the UI.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
@@ -422,7 +422,7 @@
"type": "boolean"
},
"showSpinner": {
"title": "Show Spinner",
"title": "Spinner",
"description": "Show the spinner during operations.",
"markdownDescription": "Show the spinner during operations.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
@@ -489,14 +489,14 @@
"type": "object",
"properties": {
"enabled": {
"title": "IDE Mode",
"title": "IDE",
"description": "Enable IDE integration mode.",
"markdownDescription": "Enable IDE integration mode.\n\n- Category: `IDE`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"hasSeenNudge": {
"title": "Has Seen IDE Integration Nudge",
"title": "IDE Integration Nudge",
"description": "Whether the user has seen the IDE integration nudge.",
"markdownDescription": "Whether the user has seen the IDE integration nudge.\n\n- Category: `IDE`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
@@ -513,7 +513,7 @@
"type": "object",
"properties": {
"usageStatisticsEnabled": {
"title": "Enable Usage Statistics",
"title": "Usage Statistics",
"description": "Enable collection of usage statistics",
"markdownDescription": "Enable collection of usage statistics\n\n- Category: `Privacy`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
@@ -582,18 +582,18 @@
"default": 0.5,
"type": "number"
},
"disableLoopDetection": {
"title": "Disable Loop Detection",
"description": "Disable automatic detection and prevention of infinite loops.",
"markdownDescription": "Disable automatic detection and prevention of infinite loops.\n\n- Category: `Model`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"loopDetection": {
"title": "Loop Detection",
"description": "Enable automatic detection and prevention of infinite loops.",
"markdownDescription": "Enable automatic detection and prevention of infinite loops.\n\n- Category: `Model`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"skipNextSpeakerCheck": {
"title": "Skip Next Speaker Check",
"description": "Skip the next speaker check.",
"markdownDescription": "Skip the next speaker check.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"nextSpeakerCheck": {
"title": "Next Speaker Check",
"description": "Enable the next speaker check.",
"markdownDescription": "Enable the next speaker check.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
}
},
@@ -1187,14 +1187,14 @@
"type": "string"
},
"includeDirectoryTree": {
"title": "Include Directory Tree",
"title": "Directory Tree",
"description": "Whether to include the directory tree of the current working directory in the initial request to the model.",
"markdownDescription": "Whether to include the directory tree of the current working directory in the initial request to the model.\n\n- Category: `Context`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"discoveryMaxDirs": {
"title": "Memory Discovery Max Dirs",
"title": "Discovery Max Dirs",
"description": "Maximum number of directories to search for memory.",
"markdownDescription": "Maximum number of directories to search for memory.\n\n- Category: `Context`\n- Requires restart: `no`\n- Default: `200`",
"default": 200,
@@ -1211,7 +1211,7 @@
}
},
"loadMemoryFromIncludeDirectories": {
"title": "Load Memory From Include Directories",
"title": "Memory From Include Directories",
"description": "Controls how /memory refresh loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used.",
"markdownDescription": "Controls how /memory refresh loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used.\n\n- Category: `Context`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
@@ -1225,28 +1225,28 @@
"type": "object",
"properties": {
"respectGitIgnore": {
"title": "Respect .gitignore",
"title": ".gitignore",
"description": "Respect .gitignore files when searching.",
"markdownDescription": "Respect .gitignore files when searching.\n\n- Category: `Context`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"respectGeminiIgnore": {
"title": "Respect .geminiignore",
"title": ".geminiignore",
"description": "Respect .geminiignore files when searching.",
"markdownDescription": "Respect .geminiignore files when searching.\n\n- Category: `Context`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"enableRecursiveFileSearch": {
"title": "Enable Recursive File Search",
"title": "Recursive File Search",
"description": "Enable recursive file search functionality when completing @ references in the prompt.",
"markdownDescription": "Enable recursive file search functionality when completing @ references in the prompt.\n\n- Category: `Context`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"enableFuzzySearch": {
"title": "Enable Fuzzy Search",
"title": "Fuzzy Search",
"description": "Enable fuzzy search when searching for files.",
"markdownDescription": "Enable fuzzy search when searching for files.\n\n- Category: `Context`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
@@ -1289,7 +1289,7 @@
"type": "object",
"properties": {
"enableInteractiveShell": {
"title": "Enable Interactive Shell",
"title": "Interactive Shell",
"description": "Use node-pty for an interactive shell experience. Fallback to child_process still applies.",
"markdownDescription": "Use node-pty for an interactive shell experience. Fallback to child_process still applies.\n\n- Category: `Tools`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
@@ -1303,7 +1303,7 @@
"type": "string"
},
"showColor": {
"title": "Show Color",
"title": "Color",
"description": "Show color in shell output.",
"markdownDescription": "Show color in shell output.\n\n- Category: `Tools`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
@@ -1317,7 +1317,7 @@
"type": "number"
},
"enableShellOutputEfficiency": {
"title": "Enable Shell Output Efficiency",
"title": "Shell Output Efficiency",
"description": "Enable shell output efficiency optimizations for better performance.",
"markdownDescription": "Enable shell output efficiency optimizations for better performance.\n\n- Category: `Tools`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
@@ -1366,7 +1366,7 @@
"type": "string"
},
"useRipgrep": {
"title": "Use Ripgrep",
"title": "Ripgrep",
"description": "Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.",
"markdownDescription": "Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.\n\n- Category: `Tools`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
@@ -1379,11 +1379,11 @@
"default": 40000,
"type": "number"
},
"disableLLMCorrection": {
"title": "Disable LLM Correction",
"description": "Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct.",
"markdownDescription": "Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct.\n\n- Category: `Tools`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"llmCorrection": {
"title": "LLM Correction",
"description": "Enable LLM-based error correction for edit tools. When enabled, tools will attempt to self-correct if exact string matches are not found.",
"markdownDescription": "Enable LLM-based error correction for edit tools. When enabled, tools will attempt to self-correct if exact string matches are not found.\n\n- Category: `Tools`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
}
},
@@ -1437,31 +1437,31 @@
"default": {},
"type": "object",
"properties": {
"disableYoloMode": {
"title": "Disable YOLO Mode",
"description": "Disable YOLO mode, even if enabled by a flag.",
"markdownDescription": "Disable YOLO mode, even if enabled by a flag.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"yoloModeAllowed": {
"title": "YOLO Mode",
"description": "Allow YOLO mode to be used.",
"markdownDescription": "Allow YOLO mode to be used.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"enablePermanentToolApproval": {
"title": "Allow Permanent Tool Approval",
"title": "Permanent Tool Approval",
"description": "Enable the \"Allow for all future sessions\" option in tool confirmation dialogs.",
"markdownDescription": "Enable the \"Allow for all future sessions\" option in tool confirmation dialogs.\n\n- Category: `Security`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"blockGitExtensions": {
"title": "Blocks extensions from Git",
"description": "Blocks installing and loading extensions from Git.",
"markdownDescription": "Blocks installing and loading extensions from Git.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"gitExtensionsEnabled": {
"title": "Git Extensions",
"description": "Allow installing and loading extensions from Git.",
"markdownDescription": "Allow installing and loading extensions from Git.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"allowedExtensions": {
"title": "Extension Source Regex Allowlist",
"description": "List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.",
"markdownDescription": "List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `[]`",
"description": "List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the gitExtensionsEnabled setting.",
"markdownDescription": "List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the gitExtensionsEnabled setting.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `[]`",
"default": [],
"type": "array",
"items": {
@@ -1513,7 +1513,7 @@
}
},
"enabled": {
"title": "Enable Environment Variable Redaction",
"title": "Environment Variable Redaction",
"description": "Enable redaction of environment variables that may contain secrets.",
"markdownDescription": "Enable redaction of environment variables that may contain secrets.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
@@ -1542,7 +1542,7 @@
"type": "string"
},
"useExternal": {
"title": "Use External Auth",
"title": "External Auth",
"description": "Whether to use an external authentication flow.",
"markdownDescription": "Whether to use an external authentication flow.\n\n- Category: `Security`\n- Requires restart: `yes`",
"type": "boolean"
@@ -1614,7 +1614,7 @@
"type": "object",
"properties": {
"enabled": {
"title": "Enable Tool Output Masking",
"title": "Tool Output Masking",
"description": "Enables tool output masking to save tokens.",
"markdownDescription": "Enables tool output masking to save tokens.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
@@ -1645,7 +1645,7 @@
"additionalProperties": false
},
"enableAgents": {
"title": "Enable Agents",
"title": "Agents",
"description": "Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents",
"markdownDescription": "Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
@@ -1687,14 +1687,14 @@
"type": "boolean"
},
"useOSC52Paste": {
"title": "Use OSC 52 Paste",
"title": "OSC 52 Paste",
"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).",
"markdownDescription": "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).\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"useOSC52Copy": {
"title": "Use OSC 52 Copy",
"title": "OSC 52 Copy",
"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).",
"markdownDescription": "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).\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
@@ -1803,7 +1803,7 @@
"type": "object",
"properties": {
"enabled": {
"title": "Enable Agent Skills",
"title": "Agent Skills",
"description": "Enable Agent Skills.",
"markdownDescription": "Enable Agent Skills.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
@@ -1830,7 +1830,7 @@
"type": "object",
"properties": {
"enabled": {
"title": "Enable Hooks",
"title": "Hooks",
"description": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.",
"markdownDescription": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,