Skip to content

fix(cli): keep useInputHistoryStore state updaters pure - #29098

Open
Eswar809 wants to merge 2 commits into
google-gemini:mainfrom
Eswar809:fix/input-history-pure-state-updates
Open

fix(cli): keep useInputHistoryStore state updaters pure#29098
Eswar809 wants to merge 2 commits into
google-gemini:mainfrom
Eswar809:fix/input-history-pure-state-updates

Conversation

@Eswar809

Copy link
Copy Markdown
Contributor

TLDR

useInputHistoryStore.addInput() scheduled setPastSessionMessages() and the side-effecting recalculateHistory() (which itself calls setInputHistory()) inside the setCurrentSessionMessages() updater function. React state updaters must be pure — they may be double-invoked under StrictMode and re-invoked under batching, so the nested work ran multiple times per submit.

What changed

  • recalculateHistory() now runs in a useEffect keyed to the committed currentSessionMessages / pastSessionMessages values, so it executes exactly once per committed change (as suggested in bug(cli): impure state updater in useInputHistoryStore schedules nested setState inside another setState updater #29046).
  • addInput() is reduced to a single pure functional update — no nested setState calls, no side effects inside updaters.
  • initializeFromLogger() no longer calls recalculateHistory() directly; the effect recomputes when the past-session messages commit.
  • Renamed the previously unused _currentSessionMessages / _pastSessionMessages bindings since they are now read by the effect.

Behavior is unchanged (the old pattern was idempotent); this removes the timing-dependent, unsupported pattern so it cannot break under concurrent features.

Testing

  • Added a regression test that batches three rapid addInput() calls in a single act() and asserts they compose in submission order.
  • All 15 tests in useInputHistoryStore.test.ts pass; lint, prettier and typecheck are clean.

Checklist

  • Verified the fix locally — lint, prettier, typecheck and unit tests all pass.

…i#29046)

addInput() scheduled setPastSessionMessages() and recalculateHistory()
inside the setCurrentSessionMessages() updater, so side effects ran
inside an updater function (double-invoked under StrictMode, once per
queued update under batching). Compute the recalculation in an effect
keyed to the committed session state instead, and reduce addInput() to
a pure functional update.
@Eswar809
Eswar809 requested a review from a team as a code owner August 26, 2026 09:40
@github-actions github-actions Bot added the size/m A medium sized PR label Aug 26, 2026
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 132
  • Additions: +80
  • Deletions: -52
  • Files changed: 2

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 improves the stability and predictability of the useInputHistoryStore hook by aligning it with React's best practices for state management. By decoupling side effects from state updates and moving recalculation logic into a reactive effect, the implementation avoids potential issues with double-invocations and concurrent rendering patterns.

Highlights

  • Pure State Updaters: Refactored useInputHistoryStore to remove side-effecting calls from state updater functions, ensuring they remain pure and compliant with React's concurrency requirements.
  • Effect-based Recalculation: Moved the recalculateHistory logic into a useEffect hook that triggers whenever session messages change, ensuring consistent and predictable execution.
  • Regression Testing: Added a new test case to verify that rapid, batched input submissions are correctly composed in the expected order.
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 the 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 counterproductive. 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.

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.

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

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.

Code Review

This pull request refactors the useInputHistoryStore hook to handle rapid, batched inputs correctly and adds a corresponding test case. However, the implementation introduces a useEffect hook to synchronize and recalculate the derived inputHistory state. The reviewer correctly identified this as an anti-pattern that can lead to double rendering, terminal flickering in the Ink CLI framework, and state inconsistency. It is highly recommended to simplify the hook by deriving inputHistory directly during render using useMemo instead.

Comment on lines +64 to +69
useEffect(() => {
recalculateHistory(
currentSessionMessages.slice().reverse(), // Convert to newest first
pastSessionMessages,
);
}, [currentSessionMessages, pastSessionMessages, recalculateHistory]);

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.

high

Anti-pattern: Synchronizing State with useEffect (Derived State)

Using useEffect to synchronize state (inputHistory) when other states (currentSessionMessages or pastSessionMessages) change is a known React anti-pattern.

Why this is a High-Severity Issue:

  1. Double Rendering & CLI Performance: Updating state inside useEffect forces React to perform an immediate second render pass. In a CLI application using Ink, extra render passes can cause terminal flickering, cursor glitches, or unnecessary redraw overhead.
  2. State Inconsistency: During the first render pass, inputHistory will contain stale data while currentSessionMessages has already updated. Any component relying on both will see inconsistent state.
  3. Unnecessary Complexity: It requires maintaining three separate states (inputHistory, currentSessionMessages, pastSessionMessages), a useEffect, and a recalculateHistory callback.

Recommended Solution:

Derive inputHistory directly during render using useMemo. This guarantees that inputHistory is always in sync, avoids the extra render pass, and simplifies the hook significantly.

Since some of the code to be removed (like the recalculateHistory definition and the initial useState for inputHistory) lies outside the current diff hunks, you will need to manually apply these changes across the file:

  1. Remove the inputHistory state:
    // Remove this line:
    // const [inputHistory, setInputHistory] = useState<string[]>([]);
  2. Remove the recalculateHistory callback entirely.
  3. Remove the useEffect block.
  4. Add the useMemo derivation:
    const inputHistory = useMemo(() => {
      const currentSession = currentSessionMessages.toReversed(); // Node 20+ supports toReversed()
      const combinedMessages = [...currentSession, ...pastSessionMessages];
    
      const deduplicatedMessages: string[] = [];
      if (combinedMessages.length > 0) {
        deduplicatedMessages.push(combinedMessages[0]);
        for (let i = 1; i < combinedMessages.length; i++) {
          if (combinedMessages[i] !== combinedMessages[i - 1]) {
            deduplicatedMessages.push(combinedMessages[i]);
          }
        }
      }
    
      return deduplicatedMessages.reverse();
    }, [currentSessionMessages, pastSessionMessages]);

@gemini-cli gemini-cli Bot added the area/core Issues related to User Interface, OS Support, Core Functionality label Aug 26, 2026
…via effect

Address gemini-code-assist review: synchronizing inputHistory state in
a useEffect is the derived-state anti-pattern (extra render pass,
stale intermediate state, terminal flicker under Ink). Extract the
deduplication into a pure computeInputHistory() helper and derive
inputHistory with useMemo during render.
@Eswar809

Copy link
Copy Markdown
Contributor Author

Good catch — agreed. Refactored in 6b3e357: inputHistory is now derived during render via useMemo over the committed session state (using a new pure computeInputHistory() helper), so the useEffect state-sync anti-pattern, the extra render pass, and the stale intermediate state are all gone. Added direct unit tests for computeInputHistory() as well.

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 size/m A medium sized PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant