Skip to content

Comments

refactor(cli): Reactive useSettingsStore hook#14915

Merged
jacob314 merged 7 commits intogoogle-gemini:mainfrom
psinha40898:pyush/refactor/settings-4
Feb 11, 2026
Merged

refactor(cli): Reactive useSettingsStore hook#14915
jacob314 merged 7 commits intogoogle-gemini:mainfrom
psinha40898:pyush/refactor/settings-4

Conversation

@psinha40898
Copy link
Contributor

@psinha40898 psinha40898 commented Dec 10, 2025

Summary

The SettingsDialog is bloated through excessive use of React State, Effect, unnecessary redundant states, and the use of React Anti Patterns to sync the UI with the LoadedSettings.

Anti Pattern:
SettingsContext is a thin DI for a mutable object, forcing a React anti-pattern in SettingsDialog: updating state manually after mutation instead of one idiomatic update.

Before the simple refactors, it makes sense to incrementally merge the requirements to remove this Anti Pattern from UI that is Reactive with Settings.

Details

PR1: Convert LoadedSettings to a store and create a second hook in SettingsContext that re-renders when a settings changed event is emitted

Problem: Components needing to re-render on settings changes (like SettingsDialog) currently use an anti-pattern: mutate LoadedSettings, then manually call setState to trigger re-render.

Solution:
Convert LoadedSettings to a store

  • It manages an immutable snapshot
  • It exposes a subscribe function that allows React's useSyncExternalStore to subscribe to the event bus. Whenever a settings-changed event is emitted, React checks if the snapshot has changed in object identity. If it has, it triggers the re-render we need.
const { settings, setSetting } = useSettingsStore();
// settings is reactive - re-renders on change
// setSetting is the single mutation point

Why a second hook: Exposing this as a new hook (useSettingsStore) rather than changing useSettings means:

  • Zero code churn for existing consumers
  • Opt-in reactivity only where needed

Files changed:

  • packages/cli/src/config/settings.ts
  • packages/cli/src/config/settings.test.ts
  • packages/cli/src/ui/contexts/SettingsContext.tsx
  • packages/cli/src/ui/contexts/SettingsContext.test.tsx

Next Steps:

PR2: Refactor SettingsDialog business logic to use the reactive store

This PR will ship with one small UX change - if a user toggles all restart required settings back to the value they initially were, the restart required prompt will also toggle off (b/c it's not required to apply those settings at that state). This was just a natural extension of removing complicated logic such as synchronizing many React states and batching mutations.

Replaces the imperative state-syncing model in SettingsDialog with the reactive useSettingsStore() from PR1.

  • Deletes 5 local states (pendingSettings, modifiedSettings, globalPendingChanges, _restartRequiredSettings, showRestartPrompt) and 5 obsolete utility functions from settingsUtils.ts
  • Simplifies all callbacks to call setSetting() directly — no intermediate state
  • Restart-required tracking uses a snapshot diff with comparison, only tracking dialog-visible restart settings via new getDialogRestartRequiredSettings()
  • Removes settings prop from SettingsDialog (reads from context instead)
  • Updates all tests mechanically

If the value you need can be computed entirely from the current props or other state, remove that redundant state altogether.

https://react.dev/reference/react/useState

Files changed:

  • packages/cli/src/ui/components/SettingsDialog.tsx
  • packages/cli/src/ui/components/SettingsDialog.test.tsx
  • packages/cli/src/ui/components/DialogManager.tsx
  • packages/cli/src/utils/settingsUtils.ts
  • packages/cli/src/utils/settingsUtils.test.ts
  • packages/cli/src/utils/dialogScopeUtils.ts

PR 2 against PR 1 (outdated post-merge):

psinha40898/gemini-cli@pyush/refactor/settings-4...psinha40898:gemini-cli:pyush/refactor/settings-dialog-business

204 additions and 686 deletions.

PR3: Extract hooks from BaseSettingsDialog

Pure React cleanup — no business logic changes, no external API changes.

  1. (Optional) A reducer to describe state changes for navigation in BaseSettingsDialog, wrapped in a custom hook (useDialogNavigation)

As your components grow in complexity, it can get harder to see at a glance all the different ways in which a component's state gets updated.

https://react.dev/learn/extracting-state-logic-into-a-reducer

  1. (Optional, but wrapping an effect like the cursor blink into a custom hook is considered good React) Refactors inline editing state into a custom hook (useInlineEditor) in BaseSettingsDialog

Whenever you write an Effect, consider whether it would be clearer to also wrap it in a custom Hook.

https://react.dev/learn/reusing-logic-with-custom-hooks#when-to-use-custom-hooks

  1. Derives effect when possible in BaseSettingsDialog (like the effect that forces focus on the Settings menu when the Scope menu is hidden)

You don't need Effects to transform data for rendering. (...) To avoid the unnecessary render passes, transform all the data at the top level of your components.

https://react.dev/learn/you-might-not-need-an-effect

Files changed:

  • packages/cli/src/ui/hooks/useDialogNavigation.ts (new)
  • packages/cli/src/ui/hooks/useInlineEditor.ts (new)
  • packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx

PR 3 against PR 2: (outdated post-merge):

psinha40898/gemini-cli@pyush/refactor/settings-dialog-business...psinha40898:gemini-cli:pyush/refactor/settings-dialog-base-hooks

392 additions and 183 deletions.

Future Work:

  • Remove similar anti-patterns from AgentConfigDialog.tsx
  • An alternate buffer version of the SettingsDialog that utilizes the new Scrollable Containers

Related Issues

RELATED TO #15840

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

@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 undertakes a substantial refactor of the SettingsDialog component to align with modern React patterns. The primary goal is to simplify state management by consolidating related state into a useReducer and extracting complex UI logic into a custom hook. This approach aims to make the component more maintainable, predictable, and performant by reducing redundant state and effects.

Highlights

  • State Management Refactor: The SettingsDialog.tsx component has been significantly refactored to centralize state management using a useReducer hook. This replaces numerous useState calls for navigation, search, and pending changes, reducing useState instances from 15 to 1.
  • Custom Hook for Inline Editing: Inline editing logic (key, buffer, cursor position, and visibility) has been encapsulated into a new useInlineEdit custom hook, improving modularity and reusability. This also contributes to the reduction of useState calls.
  • Derived State and Effects: Several states and effects are now derived from existing state, adhering to React best practices. For example, showRestartPrompt is derived from globalPendingChanges, and a focus correction effect is replaced by a derived effectiveFocusSection.
  • Reduced useEffect Calls: The refactoring has led to a reduction in useEffect calls from 4 to 1, indicating a more efficient and less side-effect-prone component.
  • New Reducer and Hook Implementations: Two new files, settingsDialogReducer.ts and useInlineEdit.ts, have been added to implement the new state management logic and custom hook, respectively. Both include comprehensive unit tests.
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.

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. The introduction of useReducer to manage the component's complex state and the extraction of inline editing logic into the useInlineEdit custom hook have significantly improved the code's structure, readability, and maintainability. Deriving state where possible, like for showRestartPrompt and effectiveFocusSection, is a great application of React best practices. The addition of comprehensive tests for the new reducer and hook is also a major plus, ensuring the new logic is robust.

I found one logic issue in how default values for restart-required settings are handled, which I've detailed in a specific comment. Besides that, this is a very strong pull request that greatly enhances the quality of the codebase.

@psinha40898 psinha40898 changed the title wip refactor(cli): SettingsDialog.tsx implement better React patterns by deriving state and effect and use of reducer and custom hook refactor(cli): SettingsDialog.tsx implement better React patterns by deriving state and effect and use of reducer and custom hook Dec 10, 2025
@psinha40898
Copy link
Contributor Author

The PR successfully refactors SettingsDialog.tsx to use useReducer and useInlineEdit, significantly improving state management and readability. I have reviewed the code and tests, and here are my findings.

Code Quality & Patterns

  • Refactor: The move to useReducer for complex state (navigation, search, pending changes) is excellent. It makes the state transitions explicit and testable.
  • Hooks: Extraction of useInlineEdit cleans up the component logic significantly.
  • React Anti-Pattern: The retention of pendingSettings to drive renders (due to the mutable nature of Settings) is acceptable in this context, as noted in the description.
  • Logging: The use of debugLogger is good.
    • Note: There is a console.error left in SettingsDialog.tsx (line 235: console.error('Failed to toggle vim mode:', error);). Consider using debugLogger or a UI error state. Given it's a catch block for an async action in the UI, it might get lost or mess up the TUI.

Tests

  • New Tests: The new tests for settingsDialogReducer and useInlineEdit are well-structured and use vitest correctly.
  • Render Wrapper: Usage of renderHook from ../../test-utils/render.js correctly wraps interactions in act, following the project's testing guidelines.
  • Existing Tests: It is great to see that SettingsDialog.test.tsx passed without modification, indicating the refactor preserved behavior.

Logic Issue (Reset to Default)

I confirmed the potential issue with resetting to default values for restart-required settings:

  • The PendingValue type in settingsDialogReducer.ts is boolean | number | string.
  • In SettingsDialog.tsx, when resetting a restart-required setting:
    if (
      (currentSetting.type === 'boolean' && typeof defaultValue === 'boolean') ||
      // ...
    ) {
       dispatch({ type: 'ADD_PENDING_CHANGE', ... });
    }
  • If defaultValue is undefined (which getDefaultValue returns for settings with no explicit default in the schema), this block is skipped.
  • Consequently, the "reset" (unsetting the value) is not tracked in globalPendingChanges, and the user's intent to reset it will be lost if they don't save immediately.
  • Suggestion: Update PendingValue to include undefined or a symbol to represent "unset", and handle this case in the reducer and saveModifiedSettings logic.
// Example fix for the PendingValue type
export type PendingValue = boolean | number | string | undefined; // undefined represents 'unset'

Recommendation

This is a high-quality refactor. Please address the console.error and the default value logic issue.

@psinha40898 psinha40898 marked this pull request as ready for review December 10, 2025 23:20
@psinha40898 psinha40898 requested a review from a team as a code owner December 10, 2025 23:20
@psinha40898 psinha40898 force-pushed the pyush/refactor/settings-4 branch from 1605c8c to 5aaf2bb Compare December 11, 2025 02:50
@psinha40898
Copy link
Contributor Author

psinha40898 commented Dec 11, 2025

Logic Issue (Reset to Default)

I confirmed the potential issue with resetting to default values for restart-required settings:

  • The PendingValue type in settingsDialogReducer.ts is boolean | number | string.
  • In SettingsDialog.tsx, when resetting a restart-required setting:
    if (
      (currentSetting.type === 'boolean' && typeof defaultValue === 'boolean') ||
      // ...
    ) {
       dispatch({ type: 'ADD_PENDING_CHANGE', ... });
    }
  • If defaultValue is undefined (which getDefaultValue returns for settings with no explicit default in the schema), this block is skipped.
  • Consequently, the "reset" (unsetting the value) is not tracked in globalPendingChanges, and the user's intent to reset it will be lost if they don't save immediately.
  • Suggestion: Update PendingValue to include undefined or a symbol to represent "unset", and handle this case in the reducer and saveModifiedSettings logic.
// Example fix for the PendingValue type
export type PendingValue = boolean | number | string | undefined; // undefined represents 'unset'

I can push a defect test a long with a fix but will note it for now and wait for a reviewer to decide whether this refactor PR should modify any SettingsDialog.test.tsx tests. Because the defect test would also fail on main.

  • Logging: The use of debugLogger is good.

    • Note: There is a console.error left in SettingsDialog.tsx (line 235: console.error('Failed to toggle vim mode:', error);). Consider using debugLogger or a UI error state. Given it's a catch block for an async action in the UI, it might get lost or mess up the TUI.

Same here because the console.error is inherited from main.

Because the focus of the PR is on React usage

@psinha40898
Copy link
Contributor Author

Thanks for the significant refactor! This cleans up the state management in SettingsDialog and aligns well with our React guidelines.

I've reviewed the changes and the discussion. Here are the items to address before merging:

  1. Reset to Default Logic (Undefined Values):
    Re: your comment about the logic issue with undefined default values: Please go ahead and include the fix and the test case in this PR. It is better to resolve this known bug now, especially since you have identified the solution.

    • Update PendingValue in settingsDialogReducer.ts:
      export type PendingValue = boolean | number | string | undefined;
    • Update the logic in SettingsDialog.tsx to handle undefined correctly.
    • Add the test case to settingsDialogReducer.test.ts.
  2. Console Error:
    Re: your comment about the inherited console.error:

    Same here because the console.error is inherited from main.

    While I understand it is pre-existing, GEMINI.md explicitly advises against console logs in production code. Since you are already modifying this file significantly, please switch it to debugLogger.log (or handle it in the UI) to be consistent with the rest of the project. It's a small change that improves code quality.

  3. Exhaustive Check:
    In settingsDialogReducer.ts, please use the checkExhaustive helper in the default case of the switch statement. This is a project pattern to ensure all action types are handled.

    • Import it from ../../utils/checks.js.
    • Usage:
      default:
        checkExhaustive(action);
        return state;

Verification:
The new tests in settingsDialogReducer.test.ts and useInlineEdit.test.ts look correct and use the proper test utils.

Great work on simplifying this component!

@psinha40898 psinha40898 force-pushed the pyush/refactor/settings-4 branch from 6d69a2e to 6b11a74 Compare December 11, 2025 07:07
@psinha40898 psinha40898 changed the title refactor(cli): SettingsDialog.tsx implement better React patterns by deriving state and effect and use of reducer and custom hook refactor(cli): SettingsDialog.tsx implement better React patterns by deriving state and effect and add use of reducer and custom hook Dec 11, 2025
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.

think this is generally on the right track. need to go a bit further to make sure we don't have any odd patterns forcing updates left. commented on the couple remaining cases where I see us forcing updates in a somewhat hacky way. if possible would be nice to remove as well.

@psinha40898 psinha40898 marked this pull request as draft December 16, 2025 08:04
@psinha40898 psinha40898 changed the title refactor(cli): SettingsDialog.tsx implement better React patterns by deriving state and effect and add use of reducer and custom hook refactor(cli): SettingsDialog and SettingsContext better React patterns Dec 16, 2025
@jacob314
Copy link
Contributor

let me know when this is ready fore view. there are a few merge conflicts.

@psinha40898 psinha40898 force-pushed the pyush/refactor/settings-4 branch from 358fadd to 6cf9ba8 Compare January 12, 2026 08:07
@psinha40898
Copy link
Contributor Author

Instead of refactoring settings to be a store it is probably better to use the event bus architecture in place

@psinha40898 psinha40898 changed the title refactor(cli): LoadedSettings as a Store to set foundation for Idiomatic Reactivity in cases of anti patterns refactor(cli): Add Reactive useSettingsStore hook Jan 12, 2026
@psinha40898 psinha40898 changed the title refactor(cli): Add Reactive useSettingsStore hook refactor(cli): Reactive useSettingsStore hook Jan 12, 2026
@bdmorgan
Copy link
Collaborator

Hi @psinha40898, thank you so much for your contribution to Gemini CLI! We really appreciate the time and effort you've put into this.

We're making some updates to our contribution process to improve how we track and review changes. Please take a moment to review our recent discussion post: Improving Our Contribution Process & Introducing New Guidelines.

Key Update: Starting January 26, 2026, the Gemini CLI project will require all pull requests to be associated with an existing issue. Any pull requests not linked to an issue by that date will be automatically closed.

Thank you for your understanding and for being a part of our community!

…ve updates

Implemented the external store pattern in LoadedSettings to support
reactive companion updates via useSyncExternalStore.

- Added subscribe and getSnapshot methods to LoadedSettings
- Implemented immutable snapshotting using structuredClone
- Wrapped the existing event bus for store subscriptions
- Added useSettingsStore reactive hook to SettingsContext
@psinha40898 psinha40898 force-pushed the pyush/refactor/settings-4 branch from 6cf9ba8 to e6d3c6c Compare January 19, 2026 07:49
@psinha40898 psinha40898 marked this pull request as ready for review January 19, 2026 09:49
@gemini-cli
Copy link
Contributor

gemini-cli bot commented Jan 24, 2026

Hi there! Thank you for your contribution to Gemini CLI.

To improve our contribution process and better track changes, we now require all pull requests to be associated with an existing issue, as announced in our recent discussion and as detailed in our CONTRIBUTING.md.

This pull request is being closed because it is not currently linked to an issue. You can easily reopen this PR once you have linked it to an issue.

How to link an issue:
Add a keyword followed by the issue number (e.g., Fixes #123) in the description of your pull request. For more details, see the GitHub Documentation.

Thank you for your understanding and for being a part of our community!

@gemini-cli gemini-cli bot closed this Jan 24, 2026
@jacob314 jacob314 reopened this Feb 2, 2026
@gemini-cli gemini-cli bot added 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 2, 2026
@jacob314
Copy link
Contributor

Pull Request Review: refactor(cli): Reactive useSettingsStore hook

Overall, this is a very high-quality refactor that effectively addresses the reactive settings anti-pattern. The introduction of useSyncExternalStore is the idiomatic React way to handle external state subscription in this codebase.

Highlights:

  • Architectural Improvement: The shift from imperative state syncing to a reactive store significantly simplifies component logic, especially in complex areas like the SettingsDialog.
  • Idiomatic React: Proper use of useSyncExternalStore and useMemo ensures efficient re-renders and stable object identities.
  • Solid Testing: The new tests correctly utilize our specialized renderHook and render utilities from packages/cli/src/test-utils/render.js, ensuring proper act() wrapping and avoiding test flakiness.

Suggested Improvement:

  • Exhaustiveness Checking: In packages/cli/src/ui/contexts/SettingsContext.tsx, inside the useSettingsStore hook's forScope function, consider using checkExhaustive in the default case of the switch statement. This ensures type safety if LoadableSettingScope is expanded in the future.
// packages/cli/src/ui/contexts/SettingsContext.tsx

default:
  return checkExhaustive(scope);

Great work on this!

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

@jacob314 jacob314 enabled auto-merge February 11, 2026 23:27
@jacob314 jacob314 added this pull request to the merge queue Feb 11, 2026
Merged via the queue into google-gemini:main with commit b800869 Feb 11, 2026
25 of 40 checks passed
krsjenmt added a commit to krsjenmt/gemini-cli that referenced this pull request Feb 12, 2026
…ini/gemini-cli (#37)

* fix(cli): resolve double rendering in shpool and address vscode lint warnings (google-gemini#18704)

* feat(plan): document and validate Plan Mode policy overrides (google-gemini#18825)

* Fix pressing any key to exit select mode. (google-gemini#18421)

* fix(cli): update F12 behavior to only open drawer if browser fails (google-gemini#18829)

* feat(plan): allow skills to be enabled in plan mode (google-gemini#18817)

Co-authored-by: Jerop Kipruto <jerop@google.com>

* docs(plan): add documentation for plan mode tools (google-gemini#18827)

* Remove experimental note in extension settings docs (google-gemini#18822)

* Update prompt and grep tool definition to limit context size (google-gemini#18780)

* docs(plan): add `ask_user` tool documentation (google-gemini#18830)

* Revert unintended credentials exposure (google-gemini#18840)

* feat(core): update internal utility models to Gemini 3 (google-gemini#18773)

* feat(a2a): add value-resolver for auth credential resolution (google-gemini#18653)

* Removed getPlainTextLength (google-gemini#18848)

* More grep prompt tweaks (google-gemini#18846)

* refactor(cli): Reactive useSettingsStore hook (google-gemini#14915)

* fix(mcp): Ensure that stdio MCP server execution has the `GEMINI_CLI=1` env variable populated. (google-gemini#18832)

* fix(core): improve headless mode detection for flags and query args (google-gemini#18855)

* refactor(cli): simplify UI and remove legacy inline tool confirmation logic (google-gemini#18566)

* feat(cli): deprecate --allowed-tools and excludeTools in favor of policy engine (google-gemini#18508)

* fix(workflows): improve maintainer detection for automated PR actions (google-gemini#18869)

* refactor(cli): consolidate useToolScheduler and delete legacy implementation (google-gemini#18567)

* Update changelog for v0.28.0 and v0.29.0-preview0 (google-gemini#18819)

* fix(core): ensure sub-agents are registered regardless of tools.allowed (google-gemini#18870)

---------

Co-authored-by: Brad Dux <959674+braddux@users.noreply.github.com>
Co-authored-by: Jerop Kipruto <jerop@google.com>
Co-authored-by: Jacob Richman <jacob314@gmail.com>
Co-authored-by: Sandy Tao <sandytao520@icloud.com>
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com>
Co-authored-by: christine betts <chrstn@uw.edu>
Co-authored-by: Christian Gunderman <gundermanc@gmail.com>
Co-authored-by: Adam Weidman <65992621+adamfweidman@users.noreply.github.com>
Co-authored-by: Dev Randalpura <devrandalpura@google.com>
Co-authored-by: Pyush Sinha <pyushsinha20@gmail.com>
Co-authored-by: Richie Foreman <richie.foreman@gmail.com>
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
Co-authored-by: Abhijit Balaji <abhijitbalaji@google.com>
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
Co-authored-by: g-samroberts <158088236+g-samroberts@users.noreply.github.com>
Co-authored-by: matt korwel <matt.korwel@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

Development

Successfully merging this pull request may close these issues.

3 participants