Skip to content

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

Open
psinha40898 wants to merge 28 commits intogoogle-gemini:mainfrom
psinha40898:pyush/refactor/react-settings-dialog
Open

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

Conversation

@psinha40898
Copy link
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

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
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
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
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
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
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
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
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
@psinha40898 psinha40898 marked this pull request as ready for review February 21, 2026 05:15
@psinha40898 psinha40898 changed the title refactor(cli): fully remove React anti patterns, improve type safety ergonomics and fix UX oversights in SettingsDialog.tsx refactor(cli): fully remove React anti patterns, improve type safety and fix UX oversights in SettingsDialog.tsx Feb 21, 2026
@psinha40898
Copy link
Contributor Author

/gemini review

Copy link
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

The pull request refactors the SettingsDialog to use a more reactive approach with useSettingsStore and derived state, which simplifies the component and fixes several UX issues. It also improves type safety and adds support for editing array and object settings. However, a regression was found in the commit handler for inline editing that prevents setting values to 0 or empty strings.

['System', settings.system.settings],
];

// Iterate through the nested map snapshot in activeRestartRequiredSettings, diff with current settings
Copy link
Contributor Author

@psinha40898 psinha40898 Feb 23, 2026

Choose a reason for hiding this comment

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

The diffing of restart required settings across scope here is the heart of the UX. This diffs the settings files on component mount with the current settings (updated as soon as a user toggles)

The purpose of the diff is to handle the user experience for restart required settings

Alternatively, we could read the restart required settings that are currently active to use changes pending against application restart instead of changes pending against component re mount, but this would require some greater code changes.

isSettingModified,
// Business logic utilities,
TEST_ONLY,
settingExistsInScope,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Utils that were designed to interact with bloated react states and mutable objects were all removed

export function getEffectiveValue(
key: string,
settings: Settings,
mergedSettings: Settings,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this guy was never called with this parameter in the component code. he was always called with an empty mergedSettings which meant it never checked for an inherited value.

/**
* Gets a value from a nested object using a key path array iteratively.
*/
export function getNestedValue(obj: unknown, path: string[]): unknown {
Copy link
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
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
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
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.

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

3 participants