Skip to content

refactor(cli): fully remove React anti patterns, improve type safety and fix UX oversights in SettingsDialog.tsx#18963

Merged
jacob314 merged 32 commits intogoogle-gemini:mainfrom
psinha40898:pyush/refactor/react-settings-dialog
Mar 2, 2026
Merged

refactor(cli): fully remove React anti patterns, improve type safety and fix UX oversights in SettingsDialog.tsx#18963
jacob314 merged 32 commits intogoogle-gemini:mainfrom
psinha40898:pyush/refactor/react-settings-dialog

Conversation

@psinha40898
Copy link
Copy Markdown
Contributor

@psinha40898 psinha40898 commented Feb 12, 2026

Summary

Next step from #14915
Removes excessive state and implements maintainable React and type safety patterns in the SettingsDialog component

The approach of synchronizing loose states and batching saves results in code that has hard to read concerns and becomes hard to maintain.

Examples of this can be seen in the restart required prompt which currently
does not toggle off if you toggle the setting back to what it was
does toggle off if you change a default setting to a written value and then press control L
does not toggle on if you use control L to change a non default restart required setting to default

The new dialog would be really easy to follow:

user changes Setting in the UI -> useSettingsStore triggers a re render -> the component diffs the initial restart required keys with the current -> user sees updated view

Details

The SettingsDialog and its corresponding settingsUtil are extremely bloated. Some of this stems from the reliance of a React Anti Pattern that can be removed after #14915

Instead of managing and syncing many loose React states the SettingsDialog now works in a sensible way.

  • The Dialog captures the effective state of all restart required settings that show in the dialog on mount. This represents the current session's instantiation of these settings which require a restart to change.
  • The Reactive store hook instantly updates settings and re renders the component which calculates the diff
  • From there it derives whether a restart required prompt should show

This removed many inconsistencies like

Notes:
This retains the current scope-aligned UX, so the restart required prompt will remain active if you add something to scope and toggle it back because that does not remove it from scope. But if you press Control L it will remove the prompt.
Similarly, asterisks continue to show next to Settings that exist in scope (same as main)

Aside from the above improvements, the SettingsDialog retains UX parity with the old implementation.
So stars still appear next to Settings that exist in scope.

final side notes:
this PR simplifies and improves the type safety and the three es-lint ignores.
It has some very sparse changes to AgentConfigDialog to support the new setNestedValue util for similar dialogs
i renamed settingValueExistsInScope to isInSettingsScope

Related Issues

RELATED TO #15840
Fixes #18980
Fixes #19093
Fixes #19232
Fixes #20077
Fixes #20795

How to Validate

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

- Replace imperative state-syncing with reactive useSettingsStore() from context
- Delete 5 local states: pendingSettings, modifiedSettings, globalPendingChanges, _restartRequiredSettings, showRestartPrompt
- Delete 5 obsolete utility functions from settingsUtils.ts
- Simplify all callbacks to call setSetting() directly (no intermediate state)
- Restart-required tracking uses snapshot diff with JSON.stringify comparison
- Only track dialog-visible restart settings via new getDialogRestartRequiredSettings()
- Remove settings prop from SettingsDialog (reads from context instead)
- Update all tests mechanically
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @psinha40898, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the SettingsDialog component in the CLI to improve its architecture and maintainability. By transitioning from a complex local state management approach to a centralized settings store using React Context, the component's code is cleaner, more reactive, and less prone to common React anti-patterns. This change streamlines how settings are accessed, modified, and persisted, leading to a more robust and efficient user interface for managing application settings.

Highlights

  • React Anti-Pattern Removal: The SettingsDialog component has been refactored to remove several React anti-patterns, primarily by eliminating prop drilling for settings and centralizing state management.
  • State Management Migration: Local state management for pending, modified, and restart-required settings within SettingsDialog.tsx has been replaced with a global settings store accessed via the useSettingsStore hook and SettingsContext.
  • Simplified Settings Interaction: Functions like saveModifiedSettings, setPendingSettingValue, and related logic have been removed, as settings changes are now handled immediately and reactively through the centralized store, simplifying the component's internal logic.
  • Test Suite Updates: The test suite for SettingsDialog has been updated to reflect the new architecture, including changes to how the component is rendered (using SettingsContext.Provider) and how setting modifications are asserted (spying on settings.setValue).
  • Display Logic Refinement: The getDisplayValue utility function has been simplified to use restartChangedKeys for indicating modified settings, making its logic more direct and aligned with the new state management.
Changelog
  • packages/cli/src/ui/components/DialogManager.tsx
    • Removed the settings prop from the SettingsDialog component.
  • packages/cli/src/ui/components/SettingsDialog.test.tsx
    • Updated imports to remove LoadedSettings and saveModifiedSettings, and added SettingsContext.
    • Removed the mock for saveModifiedSettings.
    • Modified the renderDialog helper to wrap SettingsDialog with SettingsContext.Provider and removed the settings prop from SettingsDialog.
    • Updated tests to spy on settings.setValue instead of mocking saveModifiedSettings.
    • Simplified assertions related to setting changes.
  • packages/cli/src/ui/components/SettingsDialog.tsx
    • Removed useEffect and multiple useState hooks related to local settings state management (showRestartPrompt, pendingSettings, modifiedSettings, globalPendingChanges, _restartRequiredSettings).
    • Introduced useSettingsStore() to access settings and setSetting from a global store.
    • Modified SettingsDialogProps to remove the settings prop.
    • Implemented captureRestartSnapshot and restartChangedKeys to reactively track restart-required settings changes.
    • Simplified handleItemToggle and handleEditCommit to directly use setSetting from the store.
    • Removed saveRestartRequiredSettings and related logic, as settings are now saved immediately via the store.
    • Updated handleItemClear to use setSetting directly.
    • Simplified handleClose and handleKeyPress by removing local state updates for modified/restart settings.
  • packages/cli/src/utils/dialogScopeUtils.ts
    • Updated the type definition for the settings parameter in getScopeMessageForSetting to reflect the new context-based access.
  • packages/cli/src/utils/settingsUtils.test.ts
    • Removed imports for setPendingSettingValue, hasRestartRequiredSettings, getRestartRequiredFromModified.
    • Removed tests for setPendingSettingValue, hasRestartRequiredSettings, getRestartRequiredFromModified.
    • Updated getDisplayValue tests to use restartChangedKeys instead of modifiedSettings and pendingSettings.
  • packages/cli/src/utils/settingsUtils.ts
    • Removed imports for LoadedSettings and LoadableSettingScope from config/settings.js.
    • Removed setNestedValue, setPendingSettingValue, setPendingSettingValueAny, hasRestartRequiredSettings, getRestartRequiredFromModified, and saveModifiedSettings functions.
    • Added getDialogRestartRequiredSettings to filter restart-required settings visible in the dialog.
    • Refactored getDisplayValue to use scopeSettings, mergedSettings, and restartChangedKeys for determining the display value and asterisk indicator, simplifying its logic.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@psinha40898 psinha40898 changed the title refactor(cli): remove React anti patterns and bloat in SettingsDialog.tsx WIP refactor(cli): remove React anti patterns and bloat in SettingsDialog.tsx Feb 12, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an excellent refactoring of the SettingsDialog component. Moving the complex local state management into a centralized store pattern using useSettingsStore dramatically simplifies the component, removing the anti-patterns and bloat mentioned in the pull request title. The new implementation is much cleaner, more maintainable, and less prone to state-related bugs. The logic for handling setting updates and the 'restart required' prompt is now significantly more straightforward and robust. The corresponding updates to tests and utility functions are thorough and align perfectly with the new architecture. Overall, this is a high-quality improvement to the codebase.

@gemini-cli gemini-cli bot added priority/p1 Important and should be addressed in the near term. priority/p2 Important but can be addressed in a future release. area/core Issues related to User Interface, OS Support, Core Functionality help wanted We will accept PRs from all issues marked as "help wanted". Thanks for your support! labels Feb 12, 2026
// has an object value (structuredClone breaks reference equality).
for (const key of getDialogRestartRequiredSettings()) {
const value = getEffectiveValue(key, {}, merged);
snapshot.set(key, JSON.stringify(value));
Copy link
Copy Markdown
Contributor Author

@psinha40898 psinha40898 Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON.stringify is required because some Settings (that are both restart required AND show in dialog) have Arrays as values. It's also defensive against future settings that may be both restart-required AND show in dialog and have Objects or Arrays as values.

Copy link
Copy Markdown
Contributor Author

@psinha40898 psinha40898 Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general this refactor seeks to fully implement UI functionality for settings that have non primitives as values.

It's included in this refactor because the functionality is kind of a side effect of improved type safety ergonomics just like the reactive restart prompt. It would be messy to split them up IMO. (related #19093)

@psinha40898 psinha40898 changed the title WIP refactor(cli): remove React anti patterns and bloat in SettingsDialog.tsx WIP refactor(cli): fully remove React anti patterns, poor type safety ergonomics, and bloat in SettingsDialog.tsx Feb 16, 2026
@psinha40898 psinha40898 changed the title WIP refactor(cli): fully remove React anti patterns, poor type safety ergonomics, and bloat in SettingsDialog.tsx WIP refactor(cli): fully remove React anti patterns and poor type safety ergonomics in SettingsDialog.tsx Feb 16, 2026
Copy link
Copy Markdown
Contributor Author

@psinha40898 psinha40898 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from Gemini

Excellent work on this refactoring! Transitioning to a centralized settings store significantly simplifies the SettingsDialog component and eliminates several React anti-patterns. The code is much cleaner, more maintainable, and the improved type safety for complex types like arrays and objects is a great addition. All tests passed locally, and the logic for handling immediate saves while tracking restart-required settings is robust. Once you're ready to take this out of draft, it's safe to merge.

/** Raw value for edit mode initialization */
rawValue?: string | number | boolean;
rawValue?: SettingsValue;
/** Optional pre-formatted edit buffer value for complex types */
Copy link
Copy Markdown
Contributor Author

@psinha40898 psinha40898 Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simple conversion of rawValue to string works for primitives but some settings in the UI have values that are not primitives, like arrays

editValue allows SettingsDialog to handle the parsing and pass it down to BaseSettingsDialog which allows for non primitive settings like arrays to work through the UI.

@psinha40898 psinha40898 changed the title WIP refactor(cli): fully remove React anti patterns and poor type safety ergonomics in SettingsDialog.tsx refactor(cli): fully remove React anti patterns and poor type safety ergonomics in SettingsDialog.tsx Feb 16, 2026
@psinha40898 psinha40898 marked this pull request as ready for review February 16, 2026 10:56
@psinha40898 psinha40898 requested a review from a team as a code owner February 16, 2026 10:56
return rawValue.join(', ');
}

if (type === 'object' && rawValue !== null && typeof rawValue === 'object') {
Copy link
Copy Markdown
Contributor Author

@psinha40898 psinha40898 Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ships with support for Settings values that are objects, but no such setting exists in the UI yet

@psinha40898 psinha40898 changed the title refactor(cli): fully remove React anti patterns and poor type safety ergonomics in SettingsDialog.tsx refactor(cli): fully remove React anti patterns and improve type safety ergonomics in SettingsDialog.tsx Feb 16, 2026
@psinha40898 psinha40898 marked this pull request as draft February 16, 2026 22:05
/**
* Gets a value from a nested object using a key path array iteratively.
*/
export function getNestedValue(obj: unknown, path: string[]): unknown {
Copy link
Copy Markdown
Contributor Author

@psinha40898 psinha40898 Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes type assertions by accepting unknown. Notably iterating down a type of the previous unsafe assertion Record<string, unknown> results in iterating into unknown anyway.

The type predicate in isRecord removes the eslint disable because our type predicate narrows the type instead of an assertion

I think there has got to be a better way with purely Typescript compile but it might require changes to the settings schemas and could be done separately.

For now replacing eslint ignores and type assertions with two type predicates seems to at least document whats going on much better

if (value !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return value as SettingsValue;
const value = getNestedValue(settings, path);
Copy link
Copy Markdown
Contributor Author

@psinha40898 psinha40898 Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here, the type predicate replaces the eslint disables and type assertions potentially pending larger scale changes

}

/**
* Get a nested value from an object using a path array
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes here can be reverted to reduce the scope of the PR.

@jacob314 jacob314 enabled auto-merge February 24, 2026 18:22
auto-merge was automatically disabled February 25, 2026 01:48

Head branch was pushed to by a user without write access

Copy link
Copy Markdown
Contributor

@jacob314 jacob314 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Approved once fix for alternate buffer mode lands.

Comment on lines +313 to +314
toggleVimEnabled().catch((error) => {
coreEvents.emitFeedback('error', 'Failed to toggle vim mode:', error);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@psinha40898 @jacob314

While testing locally, I caught a subtle edge case
that breaks the intended general.vimMode scope isolation!

In here (line 313 ) and line 348 calling toggleVimEnabled() successfully updates the runtime Vim UI state.

However, it unintentionally writes the setting to the user's global scope
because the pre-existing toggleVimEnabled function inVimModeContext.tsx
hardcodes SettingScope.User at L56.

The resulting bug:

  1. User opens Settings, correctly selects the Workspace scope.
  2. User toggles general.vimMode.
  3. SettingsDialog.tsx rightly targets the Workspace:
    setSetting('Workspace', ...)
  4. SettingsDialog then explicitly calls toggleVimEnabled() to update the
    UI on the fly.
  5. VimModeContext.tsx strictly executes settings.setValue('User', ...)
  6. Result: The user intended to update their local workspace, but
    accidentally overwrote their global configuration too.

Suggested Minimal Fix

Since SettingsDialog already handles the correct scoped file write perfectly,
we just need VimModeContext to update its React state without touching the
disk again.

1. Export a new UI-only method in VimModeContext.tsx:

 interface VimModeContextType {
   vimEnabled: boolean;
   vimMode: VimMode;
   toggleVimEnabled: () => Promise<boolean>;
+   setRuntimeVimEnabled: (newValue: boolean) => void;
   setVimMode: (mode: VimMode) => void;
 }

// ... inside VimModeProvider (around line 60) ...

+  const setRuntimeVimEnabled = useCallback((newValue: boolean) => {
+    setVimEnabled(newValue);
+    if (newValue) {
+      setVimMode('INSERT');
+    }
+  }, []);
+
   const value = {
     vimEnabled,
     vimMode,
     toggleVimEnabled,
+     setRuntimeVimEnabled,
     setVimMode,
   };

2. Swap the calls in SettingsDialog.tsx: Instead of calling
toggleVimEnabled(), grab the new method from the context:

const { vimEnabled, setRuntimeVimEnabled } = useVimMode();

And replace the calls at line 313 and line 348 with
setRuntimeVimEnabled(newValue);.

Copy link
Copy Markdown
Contributor Author

@psinha40898 psinha40898 Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for testing this out! I think the best solution might actually be removing the special case from the dialog, treating vim as a regular setting. The store pattern should take care of the rest.

[edited]

Notably this is not any regression it's just an oversight, another case where a UX bug can be fixed by simply removing code and letting the store pattern do its thing. So this bug is actually on main too

Thank you

@jacob314 jacob314 enabled auto-merge March 2, 2026 21:13
@jacob314 jacob314 added this pull request to the merge queue Mar 2, 2026
Merged via the queue into google-gemini:main with commit 8133d63 Mar 2, 2026
27 checks passed
BryanBradfo pushed a commit to BryanBradfo/gemini-cli that referenced this pull request Mar 5, 2026
…and fix UX oversights in SettingsDialog.tsx (google-gemini#18963)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
struckoff pushed a commit to struckoff/gemini-cli that referenced this pull request Mar 6, 2026
…and fix UX oversights in SettingsDialog.tsx (google-gemini#18963)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
liamhelmer pushed a commit to badal-io/gemini-cli that referenced this pull request Mar 12, 2026
…and fix UX oversights in SettingsDialog.tsx (google-gemini#18963)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality help wanted We will accept PRs from all issues marked as "help wanted". Thanks for your support! priority/p1 Important and should be addressed in the near term. priority/p2 Important but can be addressed in a future release.

Projects

None yet

4 participants