Skip to content

fix(cli): clear stale retry/loading state after cancellation (#21096)#21960

Merged
devr0306 merged 8 commits intogoogle-gemini:mainfrom
Aaxhirrr:fix/21096-cancel-retry-loading-main
Apr 2, 2026
Merged

fix(cli): clear stale retry/loading state after cancellation (#21096)#21960
devr0306 merged 8 commits intogoogle-gemini:mainfrom
Aaxhirrr:fix/21096-cancel-retry-loading-main

Conversation

@Aaxhirrr
Copy link
Copy Markdown
Contributor

@Aaxhirrr Aaxhirrr commented Mar 11, 2026

Summary

Fixes the stuck loading-state bug after cancel (Esc) for issue #21096.

The root cause was a race: canceling a request could still allow a late retry event to update UI state, so the app kept showing "This is taking a bit longer, we're still on it." even when the turn was already canceled/idle.

  • Ensures canceled turns fully reset loading state so users can immediately continue with a new prompt.

Details

  • Core retry safety
    • Added an abort guard before every onRetry(...) callback in retryWithBackoff.
    • If the request is already canceled, retry events are not emitted and an AbortError is thrown immediately.
  • UI state hardening
    • In useGeminiStream, RetryAttempt events are ignored when the turn is canceled or not actively responding.
    • retryStatus is cleared immediately during cancelOngoingRequest (instead of only waiting for isResponding effects).
  • Loading indicator correctness
    • Retry phrase now renders only in StreamingState.Responding.
    • (esc to cancel, ...) timer/help text now renders only in StreamingState.Responding.

This keeps cancellation behavior clean and prevents stale retry/loading text from leaking into the next turn.

Regression coverage

  • Added tests for:
    • abort-before-retry-callback behavior,
    • late retry events after cancel,
    • no retry phrase in Idle,
    • no cancel/timer hint in Idle.

Related Issues

How to Validate

  1. Repro manually:

  2. Start a prompt on a model likely to retry/back off.

  3. Wait until loading/retry text appears.

  4. Press Esc to cancel.

  5. Send another prompt.

  6. Confirm old retry/loading text is gone and UI is reset.

  7. Run tests:

  • npm run test --workspace @google/gemini-cli-core -- src/utils/retry.test.ts
  • npm run test --workspace @google/gemini-cli -- src/ui/hooks/useGeminiStream.test.tsx -t "Retry Handling"
  • npm run test --workspace @google/gemini-cli -- src/ui/hooks/useLoadingIndicator.test.tsx src/ui/components/LoadingIndicator.test.tsx

Specific validation update (macOS smoke test, March 10, 2026):

  • Node.js: v22.14.0
  • Gemini CLI: 0.34.0-nightly.20260304.28af4e127
  • Branch: fix/21096-cancel-retry-loading-main
  • Auth: Login with Google (OAuth)
  • Model: gemini-3.1-pro-preview

Ran 5 iterations of the repro flow:

  1. send long prompt
  2. wait for retry/loading message
  3. cancel with Esc
  4. immediately send follow-up prompt

Result: PASS (5/5). No stuck "This is taking a bit longer..." line after cancel; follow-up prompts executed normally without restart.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any): none
  • 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
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 addresses a set of issues where the CLI's UI could display incorrect or stale loading and retry information after a user cancelled an ongoing operation. The changes ensure that the system's state, particularly related to retries and loading indicators, is accurately reflected and cleared upon cancellation, preventing confusing visual cues and improving the overall user experience during interactive sessions.

Highlights

  • Stale State Prevention: Prevented retry attempt events from incorrectly re-populating the loading state after a user-initiated cancellation.
  • Robust Retry Logic: Guarded the emission of retry callbacks in the core retry logic, ensuring they are not triggered if the operation's abort signal has already been set.
  • Improved UI Responsiveness: Refined the UI to display retry phrases and cancellation/timer options only when the system is actively responding to a request.
  • Enhanced Test Coverage: Added new regression tests to specifically cover scenarios involving the cancel/retry race condition and the behavior of the idle loading UI.
Changelog
  • packages/cli/src/ui/components/LoadingIndicator.test.tsx
    • Added a new test case to verify that the cancel and timer elements are not displayed when the LoadingIndicator is in an idle state, even if a loading phrase exists.
  • packages/cli/src/ui/components/LoadingIndicator.tsx
    • Modified the condition for displaying the cancel and timer content, restricting it to only show when the streamingState is Responding.
  • packages/cli/src/ui/hooks/useGeminiStream.test.tsx
    • Updated an existing test to assert that retryStatus updates are ignored when the system is not actively responding.
    • Added a new test case to confirm that late retry events are correctly ignored after a request has been cancelled.
  • packages/cli/src/ui/hooks/useGeminiStream.ts
    • Introduced a useRef to maintain a current reference to the isResponding state, addressing potential stale closure issues.
    • Implemented a guard in the handleRetryAttempt event listener to prevent retryStatus from being set if the turn has been cancelled or the system is not responding.
    • Explicitly set retryStatus to null when cancelOngoingRequest is invoked to clear any pending retry state.
  • packages/cli/src/ui/hooks/useLoadingIndicator.test.tsx
    • Added a test to ensure that the currentLoadingPhrase is undefined when the streaming state is idle, even if a retryStatus is present.
  • packages/cli/src/ui/hooks/useLoadingIndicator.ts
    • Adjusted the retryPhrase logic to only generate a phrase when the streamingState is Responding, preventing retry messages from appearing when the system is idle.
  • packages/core/src/utils/retry.test.ts
    • Added a new test to verify that the onRetry callback is not emitted if an abort signal is triggered before the retry callback can execute.
  • packages/core/src/utils/retry.ts
    • Introduced a throwIfAborted helper function to centralize the check for an aborted signal.
    • Integrated throwIfAborted checks before each onRetry callback emission within the retryWithBackoff function, ensuring that no retry events are processed if the operation has been cancelled.
Activity
  • No specific activity (comments, reviews, etc.) was provided for this pull request.
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
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 pull request effectively addresses a race condition where stale retry/loading UI could be displayed after a user cancels a request. The fix is implemented robustly at multiple levels: the core retry logic now prevents onRetry callbacks from firing on aborted operations, and the UI has been hardened to ignore retry events unless in an active Responding state. Additionally, the retry status is now explicitly cleared upon cancellation. The changes are well-tested with specific regression tests for the race conditions. The implementation is clean and follows best practices. I have no further feedback.

Note: Security Review did not run due to the size of the PR.

@gemini-cli gemini-cli bot added priority/p1 Important and should be addressed in the near term. 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 Mar 11, 2026
@Aaxhirrr
Copy link
Copy Markdown
Contributor Author

@jacob314 This should fix the issue.
Targeted tests passed on both Windows and macOS, and I’m ready to iterate on any feedback.

@VibhorGautam
Copy link
Copy Markdown

/assign

@devr0306
Copy link
Copy Markdown
Contributor

Thank you for fixing this UI loading-state race condition! The fix successfully clears stale retry statuses during cancellations and accurately guards the retryPhrase based on StreamingState.Responding. The React tests use act and waitFor exactly as required by the development rules.

I have a couple of suggestions to clean up the implementation and reduce boilerplate:

1. Simplify isResponding state with useStateAndRef

In packages/cli/src/ui/hooks/useGeminiStream.ts, you manually synchronize the isResponding state to a ref using a useEffect:

const [isResponding, setIsResponding] = useState<boolean>(false);
const isRespondingRef = useRef(isResponding);

useEffect(() => {
  isRespondingRef.current = isResponding;
}, [isResponding]);

Since the project already uses the useStateAndRef custom hook in this very file for thought and pendingHistoryItem, you can eliminate the useEffect boilerplate and simply use:

const [isResponding, isRespondingRef, setIsResponding] = useStateAndRef<boolean>(false);

2. Centralizing the Abort Check in retry.ts

In packages/core/src/utils/retry.ts, the throwIfAborted() helper is called inside the if (onRetry) { ... } blocks.

Because JavaScript execution is single-threaded, an abort signal cannot fire during the synchronous execution of the catch (error) block (between catching the error and calling onRetry). Any abort that happens after the failed network request will already be true at the start of the catch block.

To prevent unnecessary work like error classification, jitter math, and writing debugLogger.warn statements when the request is already aborted, consider adding a single if (signal?.aborted) throw createAbortError(); at the very top of the catch (error) block, and at the beginning of the if (shouldRetryOnContent(...)) block. This way, you can remove the multiple throwIfAborted() calls scattered throughout the retry logic.
(Note: You may need to slightly adjust your synthetic test that triggers an abort mid-flight via a custom getter if you make this change).

Great work on this fix!

@Aaxhirrr
Copy link
Copy Markdown
Contributor Author

Aaxhirrr commented Mar 27, 2026

Thank you for fixing this UI loading-state race condition! The fix successfully clears stale retry statuses during cancellations and accurately guards the retryPhrase based on StreamingState.Responding. The React tests use act and waitFor exactly as required by the development rules.

Great work on this fix!

@devr0306 Thanks again for the review.
I’ve now addressed both of the requested cleanup items in this PR:

  • switched isResponding over to useStateAndRef
  • centralized the abort checks in retry.ts and updated the related regression coverage accordingly

I also reran the targeted tests for the retry/cancel/loading paths after making those changes.

One scope question before I do anything further: the issue thread for #21096 has grown pretty broad, and a number of comments now seem to mix the original cancel-related stale UI bug with longer-running backend/capacity/429-related "This is taking a bit longer..." cases.

Given the current scope of this PR, would you prefer that I keep it focused on the UI loading-state race condition during cancellation, or should I expand/update this PR to try to cover the broader "taking a bit longer" reports from the issue thread as well?

If the latter would be better handled as a separate follow-up issue/PR, I’m happy to keep this one narrowly scoped and work with you to get this merged first, and then I can immediately move on to investigating any broader follow-up issues in a separate PR if needed.

Waiting for your call, and thanks for your appreciation, always happy to contribute.

EDIT:

Rebased this branch onto the latest upstream main and resolved the merge conflicts in the affected loading/retry files.

While doing that, I also aligned a few related tests with the current upstream test helper behavior so the rebased branch stays green after conflict resolution.

Re-ran the targeted validation on the rebased branch:

  • npm run test --workspace @google/gemini-cli-core -- src/utils/retry.test.ts
  • npm run test --workspace @google/gemini-cli -- src/ui/hooks/useGeminiStream.test.tsx -t "Retry Handling"
  • npm run test --workspace @google/gemini-cli -- src/ui/hooks/useLoadingIndicator.test.tsx src/ui/components/LoadingIndicator.test.tsx

All of the above passed on the rebased branch.

@Aaxhirrr Aaxhirrr force-pushed the fix/21096-cancel-retry-loading-main branch from 9eed249 to ee1ae79 Compare March 28, 2026 03:07
@Aaxhirrr
Copy link
Copy Markdown
Contributor Author

@devr0306

Addressed both follow-up review points in the latest update.

Changes:

  • restored settings.merged.billing?.overageStrategy in the handleSubmitQuery callback dependencies
  • cleaned up the retry-attempt effect dependencies to [], since the logic reads stable refs (turnCancelledRef / isRespondingRef) at event time rather than depending on state-driven re-subscription

Re-ran the targeted validation on this updated branch:

  • npm run test --workspace @google/gemini-cli-core -- src/utils/retry.test.ts
  • npm run test --workspace @google/gemini-cli -- src/ui/hooks/useGeminiStream.test.tsx -t "Retry Handling"
  • npm run test --workspace @google/gemini-cli -- src/ui/hooks/useLoadingIndicator.test.tsx src/ui/components/LoadingIndicator.test.tsx

All of the above passed on the updated branch.

Waiting for your call, and happy to make any adjustments if needed.

Copy link
Copy Markdown
Contributor

@devr0306 devr0306 left a comment

Choose a reason for hiding this comment

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

LGTM

@Aaxhirrr
Copy link
Copy Markdown
Contributor Author

Aaxhirrr commented Mar 31, 2026

LGTM

Thank you for the approval.

I see that the Lint is failing, so I'm currently working on committing one final change to address the failing lint check, and once that is pushed this should turn green.

EDIT:
Pushed a small follow-up to address the hook-deps lint failures; lint should pass now. Waiting for you to approve the workflows again so CI can rerun, and I’ll iterate further if anything else comes up.

@Aaxhirrr
Copy link
Copy Markdown
Contributor Author

@devr0306

Thanks again for the approval, and sorry for the extra churn here.

I spent the last 4 hours digging into the remaining CI failures locally before pushing this follow-up. The issue turned out not to be the core cancel/retry fix itself, but the test/formatting side around it:

  • useGeminiStream.test.tsx was causing the full CLI test shard to blow up in CI-mode, even though the narrower retry tests were passing
  • Prettier was also failing on the loading-indicator files touched by this PR

What I updated in the latest commit:

  • stabilized the useGeminiStream.test.tsx test harness so the full file runs cleanly in CI-mode again
  • fixed the formatting issues in LoadingIndicator.tsx and useLoadingIndicator.ts

I did not change the core product logic of the PR in this follow-up; this was focused on getting the PR-specific CI failures under control.

Before pushing, I reran:

  • ESLint
  • Prettier on the touched files
  • packages/core retry tests
  • loading-indicator tests
  • the full useGeminiStream.test.tsx file in CI-mode multiple times
  • the full @google/gemini-cli test:ci run locally
  • bundle build / smoke check

I’m much more confident in this update, and I believe it should pass the checks this time.

When you get a chance, could you please re-approve the workflows so CI can run again on the latest commit? Thank you.

@Aaxhirrr
Copy link
Copy Markdown
Contributor Author

Aaxhirrr commented Apr 1, 2026

Thanks again for the review.

At this point, I’ve fixed the PR-specific CI/lint/test issues, and the regular checks are now passing. The only remaining failure is E2E (Chained), so I dug through all three failing E2E variants to try to understand whether it’s actually caused by this PR.

From the logs, I’m not fully convinced that this remaining E2E failure is caused by the actual logic in this PR.

From my understanding, what I found is that the remaining failures are all in the same interactive file-system test: file-system-interactive.test.ts > should perform a read-then-write sequence.

  • Linux sandbox:none: times out waiting to observe the read_file tool call
  • macOS: same failure, also times out waiting for read_file
  • Linux sandbox:docker: gets further, but then times out waiting to observe a successful write_file / replace

What makes me unsure this is caused by this PR is that the non-interactive sibling file-system test later completes the same read-then-write flow successfully in those runs, including updating version.txt to 1.0.1.

Since this PR is scoped to the cancel/retry/loading-state race condition, and the remaining failure appears isolated to the interactive E2E path, I wanted to ask for guidance before making more changes here.

Im not sure, if you want me to go ahead and try to patch this E2E issue as part of this PR, or treat it as separate?

Happy to keep digging either way.

@devr0306 devr0306 added this pull request to the merge queue Apr 2, 2026
@devr0306
Copy link
Copy Markdown
Contributor

devr0306 commented Apr 2, 2026

Thanks for the update! Just needed to sync the branch with main and all the checks pass now. Look forward to more contributions from you in the future!

Merged via the queue into google-gemini:main with commit 77027df Apr 2, 2026
27 checks passed
@Aaxhirrr
Copy link
Copy Markdown
Contributor Author

Aaxhirrr commented Apr 2, 2026

Thanks for the update! Just needed to sync the branch with main and all the checks pass now. Look forward to more contributions from you in the future!

Sweet, glad that fixed it! Thanks for the patient code review and merge, really appreciate it.
Will definitely keep the improvements coming!

ehedlund pushed a commit that referenced this pull request Apr 3, 2026
…#21960)

Co-authored-by: Aashir Javed <Aaxhirrr@users.noreply.github.com>
Co-authored-by: Dev Randalpura <devrandalpura@google.com>
kalenkevich pushed a commit to kalenkevich/gemini-cli that referenced this pull request Apr 3, 2026
…gemini#21096) (google-gemini#21960)

Co-authored-by: Aashir Javed <Aaxhirrr@users.noreply.github.com>
Co-authored-by: Dev Randalpura <devrandalpura@google.com>
afanty2021 pushed a commit to afanty2021/gemini-cli that referenced this pull request Apr 4, 2026
…gemini#21096) (google-gemini#21960)

Co-authored-by: Aashir Javed <Aaxhirrr@users.noreply.github.com>
Co-authored-by: Dev Randalpura <devrandalpura@google.com>
warrenzhu25 pushed a commit to warrenzhu25/gemini-cli that referenced this pull request Apr 9, 2026
…gemini#21096) (google-gemini#21960)

Co-authored-by: Aashir Javed <Aaxhirrr@users.noreply.github.com>
Co-authored-by: Dev Randalpura <devrandalpura@google.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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Request gets stuck with “This is taking a bit longer, we're still on it” after canceling request

3 participants