From b44af7c16805cbd045896ddc1eae8a48c29b2ad5 Mon Sep 17 00:00:00 2001 From: Keith Guerin Date: Mon, 23 Feb 2026 15:52:04 -0800 Subject: [PATCH] refactor(cli): implement noun-first labels and positive logic for settings (#20097) --- docs/cli/settings.md | 113 ++--- docs/reference/configuration.md | 98 ++-- packages/cli/src/config/config.test.ts | 28 +- packages/cli/src/config/config.ts | 21 +- .../config/extension-manager-themes.spec.ts | 2 +- packages/cli/src/config/extension-manager.ts | 4 +- packages/cli/src/config/extension.test.ts | 16 +- .../src/config/settings-validation.test.ts | 10 +- packages/cli/src/config/settings.test.ts | 345 ++++++++------- packages/cli/src/config/settings.ts | 418 ++++++++++++------ .../cli/src/config/settingsSchema.test.ts | 14 +- packages/cli/src/config/settingsSchema.ts | 192 +++++--- .../cli/src/config/settings_repro.test.ts | 4 +- packages/cli/src/gemini.test.tsx | 4 +- packages/cli/src/gemini.tsx | 2 +- packages/cli/src/test-utils/mockConfig.ts | 4 +- packages/cli/src/ui/AppContainer.test.tsx | 58 +-- packages/cli/src/ui/AppContainer.tsx | 9 +- packages/cli/src/ui/components/AppHeader.tsx | 7 +- .../cli/src/ui/components/Composer.test.tsx | 12 +- packages/cli/src/ui/components/Composer.tsx | 12 +- .../cli/src/ui/components/Footer.test.tsx | 34 +- packages/cli/src/ui/components/Footer.tsx | 22 +- .../src/ui/components/SettingsDialog.test.tsx | 36 +- .../cli/src/ui/components/SettingsDialog.tsx | 11 +- .../src/ui/components/StatusDisplay.test.tsx | 18 +- .../cli/src/ui/components/StatusDisplay.tsx | 9 +- .../components/shared/BaseSettingsDialog.tsx | 28 ++ packages/cli/src/utils/settingsUtils.test.ts | 27 +- packages/cli/src/utils/settingsUtils.ts | 4 +- packages/core/src/config/config.test.ts | 32 +- packages/core/src/config/config.ts | 2 +- schemas/settings.schema.json | 238 +++++----- 33 files changed, 1092 insertions(+), 742 deletions(-) diff --git a/docs/cli/settings.md b/docs/cli/settings.md index faf3fca3f0..4640713e98 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -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` | -| 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. | `[]` | +| UI Label | Setting | Description | Default | +| ------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| 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` | -| 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` | +| UI Label | Setting | Description | Default | +| -------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| 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` | +| 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` | @@ -151,15 +152,15 @@ they appear in the UI. ### Skills -| UI Label | Setting | Description | Default | -| ------------------- | ---------------- | -------------------- | ------- | -| Enable Agent Skills | `skills.enabled` | Enable Agent Skills. | `true` | +| UI Label | Setting | Description | Default | +| ------------ | ---------------- | -------------------- | ------- | +| 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` | diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 6bfd69203b..56828c991e 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -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 diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 919ad86c51..1a9f260051 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -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({ diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index bbc8b1681e..018ac24f06 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -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, diff --git a/packages/cli/src/config/extension-manager-themes.spec.ts b/packages/cli/src/config/extension-manager-themes.spec.ts index b1b21aab55..a76611b9be 100644 --- a/packages/cli/src/config/extension-manager-themes.spec.ts +++ b/packages/cli/src/config/extension-manager-themes.spec.ts @@ -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, diff --git a/packages/cli/src/config/extension-manager.ts b/packages/cli/src/config/extension-manager.ts index 56152cd6e1..7e464951bd 100644 --- a/packages/cli/src/config/extension-manager.ts +++ b/packages/cli/src/config/extension-manager.ts @@ -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.`, diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index f8e66bf8e2..d73a44f590 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -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( diff --git a/packages/cli/src/config/settings-validation.test.ts b/packages/cli/src/config/settings-validation.test.ts index baf9b5bbdb..adb29b0d4b 100644 --- a/packages/cli/src/config/settings-validation.test.ts +++ b/packages/cli/src/config/settings-validation.test.ts @@ -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, }, }, }; diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 8fd0bd81b0..cd2e5308f9 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -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 }), - ); - - // Check context.fileFiltering was migrated - expect(setValueSpy).toHaveBeenCalledWith( - SettingScope.User, - 'context', expect.objectContaining({ - fileFiltering: expect.objectContaining({ enableFuzzySearch: true }), + enableAutoUpdate: true, + enableAutoUpdateNotification: false, }), ); - // Check ui.accessibility was migrated + // Verify UI migrations expect(setValueSpy).toHaveBeenCalledWith( SettingScope.User, 'ui', expect.objectContaining({ - accessibility: expect.objectContaining({ - enableLoadingPhrases: false, + windowTitle: false, + tips: true, + banner: false, + contextSummary: true, + footerEnabled: false, + footer: expect.objectContaining({ + cwd: false, + sandboxStatus: true, + modelInfo: false, + contextPercentage: true, }), }), ); - // Check that enableLoadingPhrases: false was further migrated to loadingPhrases: 'off' + // Verify model migrations expect(setValueSpy).toHaveBeenCalledWith( SettingScope.User, - 'ui', + 'model', expect.objectContaining({ - loadingPhrases: 'off', + 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, + }), }), ); }); @@ -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,120 +3037,120 @@ MALICIOUS_VAR=allowed-because-trusted }); }); }); -}); -describe('LoadedSettings Isolation and Serializability', () => { - let loadedSettings: LoadedSettings; + describe('LoadedSettings Isolation and Serializability', () => { + let loadedSettings: LoadedSettings; - interface TestData { - a: { - b: number; - }; - } - - beforeEach(() => { - vi.resetAllMocks(); - - // Create a minimal LoadedSettings instance - const emptyScope = { - path: '/mock/settings.json', - settings: {}, - originalSettings: {}, - } as unknown as SettingsFile; - - loadedSettings = new LoadedSettings( - emptyScope, // system - emptyScope, // systemDefaults - { ...emptyScope }, // user - emptyScope, // workspace - true, // isTrusted - ); - }); - - describe('setValue Isolation', () => { - it('should isolate state between settings and originalSettings', () => { - const complexValue: TestData = { a: { b: 1 } }; - loadedSettings.setValue(SettingScope.User, 'test', complexValue); - - const userSettings = loadedSettings.forScope(SettingScope.User); - const settingsValue = (userSettings.settings as Record)[ - 'test' - ] as TestData; - const originalValue = ( - userSettings.originalSettings as Record - )['test'] as TestData; - - // Verify they are equal but different references - expect(settingsValue).toEqual(complexValue); - expect(originalValue).toEqual(complexValue); - expect(settingsValue).not.toBe(complexValue); - expect(originalValue).not.toBe(complexValue); - expect(settingsValue).not.toBe(originalValue); - - // Modify the in-memory setting object - settingsValue.a.b = 2; - - // originalSettings should NOT be affected - expect(originalValue.a.b).toBe(1); - }); - - it('should not share references between settings and originalSettings (original servers test)', () => { - const mcpServers = { - 'test-server': { command: 'echo' }, + interface TestData { + a: { + b: number; }; + } - loadedSettings.setValue(SettingScope.User, 'mcpServers', mcpServers); + beforeEach(() => { + vi.resetAllMocks(); - // Modify the original object - delete (mcpServers as Record)['test-server']; + // Create a minimal LoadedSettings instance + const emptyScope = { + path: '/mock/settings.json', + settings: {}, + originalSettings: {}, + } as unknown as SettingsFile; - // The settings in LoadedSettings should still have the server - const userSettings = loadedSettings.forScope(SettingScope.User); - expect( - (userSettings.settings.mcpServers as Record)[ - 'test-server' - ], - ).toBeDefined(); - expect( - (userSettings.originalSettings.mcpServers as Record)[ - 'test-server' - ], - ).toBeDefined(); - - // They should also be different objects from each other - expect(userSettings.settings.mcpServers).not.toBe( - userSettings.originalSettings.mcpServers, + loadedSettings = new LoadedSettings( + emptyScope, // system + emptyScope, // systemDefaults + { ...emptyScope }, // user + emptyScope, // workspace + true, // isTrusted ); }); - }); - describe('setValue Serializability', () => { - it('should preserve Map/Set types (via structuredClone)', () => { - const mapValue = { myMap: new Map([['key', 'value']]) }; - loadedSettings.setValue(SettingScope.User, 'test', mapValue); + describe('setValue Isolation', () => { + it('should isolate state between settings and originalSettings', () => { + const complexValue: TestData = { a: { b: 1 } }; + loadedSettings.setValue(SettingScope.User, 'test', complexValue); - const userSettings = loadedSettings.forScope(SettingScope.User); - const settingsValue = (userSettings.settings as Record)[ - 'test' - ] as { myMap: Map }; + const userSettings = loadedSettings.forScope(SettingScope.User); + const settingsValue = ( + userSettings.settings as Record + )['test'] as TestData; + const originalValue = ( + userSettings.originalSettings as Record + )['test'] as TestData; - // Map is preserved by structuredClone - expect(settingsValue.myMap).toBeInstanceOf(Map); - expect(settingsValue.myMap.get('key')).toBe('value'); + // Verify they are equal but different references + expect(settingsValue).toEqual(complexValue); + expect(originalValue).toEqual(complexValue); + expect(settingsValue).not.toBe(complexValue); + expect(originalValue).not.toBe(complexValue); + expect(settingsValue).not.toBe(originalValue); - // But it should be a different reference - expect(settingsValue.myMap).not.toBe(mapValue.myMap); + // Modify the in-memory setting object + settingsValue.a.b = 2; + + // originalSettings should NOT be affected + expect(originalValue.a.b).toBe(1); + }); + + it('should not share references between settings and originalSettings (original servers test)', () => { + const mcpServers = { + 'test-server': { command: 'echo' }, + }; + + loadedSettings.setValue(SettingScope.User, 'mcpServers', mcpServers); + + // Modify the original object + delete (mcpServers as Record)['test-server']; + + // The settings in LoadedSettings should still have the server + const userSettings = loadedSettings.forScope(SettingScope.User); + expect( + (userSettings.settings.mcpServers as Record)[ + 'test-server' + ], + ).toBeDefined(); + expect( + (userSettings.originalSettings.mcpServers as Record)[ + 'test-server' + ], + ).toBeDefined(); + + // They should also be different objects from each other + expect(userSettings.settings.mcpServers).not.toBe( + userSettings.originalSettings.mcpServers, + ); + }); }); - it('should handle circular references (structuredClone supports them, but deepMerge may not)', () => { - const circular: Record = { a: 1 }; - circular['self'] = circular; + describe('setValue Serializability', () => { + it('should preserve Map/Set types (via structuredClone)', () => { + const mapValue = { myMap: new Map([['key', 'value']]) }; + loadedSettings.setValue(SettingScope.User, 'test', mapValue); - // structuredClone(circular) works, but LoadedSettings.setValue calls - // computeMergedSettings() -> customDeepMerge() which blows up on circularity. - expect(() => { - loadedSettings.setValue(SettingScope.User, 'test', circular); - }).toThrow(/Maximum call stack size exceeded/); + const userSettings = loadedSettings.forScope(SettingScope.User); + const settingsValue = ( + userSettings.settings as Record + )['test'] as { myMap: Map }; + + // Map is preserved by structuredClone + expect(settingsValue.myMap).toBeInstanceOf(Map); + expect(settingsValue.myMap.get('key')).toBe('value'); + + // But it should be a different reference + expect(settingsValue.myMap).not.toBe(mapValue.myMap); + }); + + it('should handle circular references (structuredClone supports them, but deepMerge may not)', () => { + const circular: Record = { a: 1 }; + circular['self'] = circular; + + // structuredClone(circular) works, but LoadedSettings.setValue calls + // computeMergedSettings() -> customDeepMerge() which blows up on circularity. + expect(() => { + loadedSettings.setValue(SettingScope.User, 'test', circular); + }).toThrow(/Maximum call stack size exceeded/); + }); }); }); }); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 657968a3b6..9fcc176337 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -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 | 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 + | 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 @@ -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,14 +1022,123 @@ export function migrateDeprecatedSettings( ) { if (!enableLP) { newUi['loadingPhrases'] = 'off'; - loadedSettings.setValue(scope, 'ui', newUi); - if (!settingsFile.readOnly) { - anyModified = true; - } + modified = true; } foundDeprecated.push('ui.accessibility.enableLoadingPhrases'); } } + + if (modified) { + loadedSettings.setValue(scope, 'ui', newUi); + if (!settingsFile.readOnly) { + anyModified = true; + } + } + } + + // Migrate model settings + const modelSettings = settings.model as Record | 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 | 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 + | 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; + } + } } // Migrate context settings @@ -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 @@ -950,43 +1162,18 @@ export function migrateDeprecatedSettings( 'enableFuzzySearch', 'context.fileFiltering', foundDeprecated, + true, ) ) { newContext['fileFiltering'] = newFileFiltering; - loadedSettings.setValue(scope, 'context', newContext); - if (!settingsFile.readOnly) { - anyModified = true; - } + modified = true; } } - } - // Migrate tools settings - const toolsSettings = settings.tools as Record | undefined; - if (toolsSettings) { - if (toolsSettings['approvalMode'] !== undefined) { - foundDeprecated.push('tools.approvalMode'); - - const generalSettings = - (settings.general as Record | 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; - } + if (modified) { + loadedSettings.setValue(scope, 'context', newContext); + if (!settingsFile.readOnly) { + anyModified = true; } } } @@ -998,6 +1185,7 @@ export function migrateDeprecatedSettings( scope, removeDeprecated, foundDeprecated, + settingsFile.readOnly ?? false, ); if (experimentalModified) { @@ -1076,106 +1264,86 @@ function migrateExperimentalSettings( loadedSettings: LoadedSettings, scope: LoadableSettingScope, removeDeprecated: boolean, - foundDeprecated?: string[], + foundDeprecated: string[] | undefined, + readOnly: boolean, ): boolean { const experimentalSettings = settings.experimental as | Record | undefined; if (experimentalSettings) { - const agentsSettings = { - ...(settings.agents as Record | undefined), - }; - const agentsOverrides = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - ...((agentsSettings['overrides'] as Record) || {}), - }; + const newExperimental = { ...experimentalSettings }; let modified = false; - const migrateExperimental = ( - oldKey: string, - migrateFn: (oldValue: Record) => 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 + | undefined; if (old) { foundDeprecated?.push(`experimental.${oldKey}`); - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - migrateFn(old as Record); + const override = + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (newExperimental[newKey] as Record) || {}; + + if (old['enabled'] !== undefined) override['enabled'] = old['enabled']; + + if ( + old['maxNumTurns'] !== undefined || + old['maxTimeMinutes'] !== undefined + ) { + const runConfig = + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (override['runConfig'] as Record) || {}; + if (old['maxNumTurns'] !== undefined) + runConfig['maxTurns'] = old['maxNumTurns']; + if (old['maxTimeMinutes'] !== undefined) + runConfig['maxTimeMinutes'] = old['maxTimeMinutes']; + override['runConfig'] = runConfig; + } + + if (old['model'] !== undefined || old['thinkingBudget'] !== undefined) { + const modelConfig = + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (override['modelConfig'] as Record) || {}; + if (old['model'] !== undefined) modelConfig['model'] = old['model']; + if (old['thinkingBudget'] !== undefined) { + const generateContentConfig = + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (modelConfig['generateContentConfig'] as Record< + string, + unknown + >) || {}; + const thinkingConfig = + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (generateContentConfig['thinkingConfig'] as Record< + string, + unknown + >) || {}; + thinkingConfig['thinkingBudget'] = old['thinkingBudget']; + generateContentConfig['thinkingConfig'] = thinkingConfig; + modelConfig['generateContentConfig'] = generateContentConfig; + } + override['modelConfig'] = modelConfig; + } + + newExperimental[newKey] = override; + + if (removeDeprecated) { + delete newExperimental[oldKey]; + } 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 - | undefined), - }; - - if (old['enabled'] !== undefined) override['enabled'] = old['enabled']; - - const runConfig = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - ...(override['runConfig'] as Record | undefined), - }; - 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; - - if (old['model'] !== undefined || old['thinkingBudget'] !== undefined) { - const modelConfig = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - ...(override['modelConfig'] as Record | undefined), - }; - if (old['model'] !== undefined) modelConfig['model'] = old['model']; - if (old['thinkingBudget'] !== undefined) { - const generateContentConfig = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - ...(modelConfig['generateContentConfig'] as - | Record - | undefined), - }; - const thinkingConfig = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - ...(generateContentConfig['thinkingConfig'] as - | Record - | undefined), - }; - thinkingConfig['thinkingBudget'] = old['thinkingBudget']; - generateContentConfig['thinkingConfig'] = thinkingConfig; - modelConfig['generateContentConfig'] = generateContentConfig; - } - 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 | undefined), - }; - if (old['enabled'] !== undefined) override['enabled'] = old['enabled']; - agentsOverrides['cli_help'] = override; - }); + migrateAgent('codebaseInvestigatorSettings', 'codebaseInvestigator'); + migrateAgent('cliHelpAgentSettings', 'cliHelp'); if (modified) { - agentsSettings['overrides'] = agentsOverrides; - loadedSettings.setValue(scope, 'agents', agentsSettings); - - if (removeDeprecated) { - const newExperimental = { ...experimentalSettings }; - delete newExperimental['codebaseInvestigatorSettings']; - delete newExperimental['cliHelpAgentSettings']; - loadedSettings.setValue(scope, 'experimental', newExperimental); + loadedSettings.setValue(scope, 'experimental', newExperimental); + if (!readOnly) { + return true; } - return true; } } return false; diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 17a916213f..dd75deab3c 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -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, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 599c8e586b..d5abc8f57b 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -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, + 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, diff --git a/packages/cli/src/config/settings_repro.test.ts b/packages/cli/src/config/settings_repro.test.ts index 36495a99c4..3b88e3fa05 100644 --- a/packages/cli/src/config/settings_repro.test.ts +++ b/packages/cli/src/config/settings_repro.test.ts @@ -169,8 +169,8 @@ describe('Settings Repro', () => { showCitations: true, useInkScrolling: true, footer: { - hideContextPercentage: false, - hideModelInfo: false, + contextPercentage: true, + modelInfo: true, }, }, useWriteTodos: true, diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 2784c5694a..f8769ba2cc 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -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, }, diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 2e238765e8..957e4931b3 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -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, diff --git a/packages/cli/src/test-utils/mockConfig.ts b/packages/cli/src/test-utils/mockConfig.ts index 8b7c7c520d..ee7ccb5749 100644 --- a/packages/cli/src/test-utils/mockConfig.ts +++ b/packages/cli/src/test-utils/mockConfig.ts @@ -104,7 +104,7 @@ export const createMockConfig = (overrides: Partial = {}): 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 => 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), diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 8505afd3ef..fc6717dbce 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -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; diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index d42cad8495..0c611ce878 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -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, ]); diff --git a/packages/cli/src/ui/components/AppHeader.tsx b/packages/cli/src/ui/components/AppHeader.tsx index ad5e2f67d2..fe58d4b9e4 100644 --- a/packages/cli/src/ui/components/AppHeader.tsx +++ b/packages/cli/src/ui/components/AppHeader.tsx @@ -38,7 +38,7 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => { return ( - {!(settings.merged.ui.hideBanner || config.getScreenReader()) && ( + {settings.merged.ui.banner && !config.getScreenReader() && ( <>
{bannerVisible && bannerText && ( @@ -53,8 +53,9 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => { {settings.merged.ui.showUserIdentity !== false && ( )} - {!(settings.merged.ui.hideTips || config.getScreenReader()) && - showTips && } + {settings.merged.ui.tips && !config.getScreenReader() && showTips && ( + + )} ); }; diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index 999b1531f9..f273adc803 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -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 }, }, }); diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 51c879e772..c272372ec1 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -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 && ( { alignItems={isNarrow ? 'flex-start' : 'flex-end'} > {!showLoadingIndicator && ( - + )} @@ -470,7 +472,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { )} {showUiDetails && - !settings.merged.ui.hideFooter && + settings.merged.ui.footerEnabled && !isScreenReaderEnabled &&