Skip to content

fix(cli): preserve scroll position and partition pending height budget - #29520

Merged
DavidAPierce merged 5 commits into
google-gemini:mainfrom
luisfelipe-alt:bugfix/WT-engineer_561555609
Oct 1, 2026
Merged

DavidAPierce merged 5 commits into
google-gemini:mainfrom
luisfelipe-alt:bugfix/WT-engineer_561555609

Conversation

@luisfelipe-alt

Copy link
Copy Markdown
Contributor

Summary

This pull request resolves viewport scroll position resets in Gemini CLI
during active streaming, tool confirmation prompts, and unconstrained height
inspection. It ensures the terminal viewport remains stable when you scroll up
to review earlier conversation history or expanded code diffs.

Details

Three coordinated UI state and layout adjustments address the underlying causes
of unexpected viewport jumps across both virtualized and standard terminal
buffer modes:

  1. Virtualized list scroll preservation (packages/cli/src/ui/components/MainContent.tsx):
    Guards scrollableListRef.current?.scrollToEnd() inside the confirmingTool
    effect by inspecting scrollableListRef.current.getScrollState?.(). The view
    now auto-scrolls to the bottom on confirmation arrival only when the list is
    already pinned to the bottom (scrollHeight - innerHeight - scrollTop <= 1),
    preserving your scroll position when you inspect earlier turns.
  2. Standard buffer height budget partitioning (packages/cli/src/ui/components/MainContent.tsx):
    Partitions availableTerminalHeight in standard buffer mode whenever visible
    pendingHistoryItems (items other than AwaitingApproval tool calls) and
    ToolConfirmationQueue render simultaneously. Allocating 40% of the budget
    across visible pending items and 60% to ToolConfirmationQueue via a scoped
    UIStateContext.Provider keeps the combined dynamic region within
    process.stdout.rows and prevents full-screen Ink redraw resets.
  3. Unconstrained height navigation stability (packages/cli/src/ui/AppContainer.tsx):
    Restricts the if (!constrainHeight) auto-collapse branch in
    handleGlobalKeypress so that it triggers only on explicit
    Command.SHOW_MORE_LINES (Ctrl+O or Ctrl+S) or Command.ESCAPE
    commands. Navigation keys such as PageUp, PageDown, and arrow keys no
    longer re-enable constrainHeight or invoke refreshStatic() while you
    review expanded output.

Related Issues

Fixes #5009

How to Validate

Run the automated unit test suites, linter, and type checker to verify the
changes across all affected components.

  1. Run the MainContent unit tests to verify scroll preservation and dynamic
    height partitioning:
    npm test -w @google/gemini-cli -- src/ui/components/MainContent.test.tsx
  2. Run the AppContainer unit tests to verify unconstrained height keyboard
    handling:
    npm test -w @google/gemini-cli -- src/ui/AppContainer.test.tsx
  3. Run the full confirmation queue and frame rendering test suites to verify
    compatibility with existing layout budgets:
    npm test -w @google/gemini-cli -- src/ui/components/ToolConfirmationQueue.test.tsx src/ui/components/ToolConfirmationFullFrame.test.tsx
  4. Run the project linter and TypeScript type checker:
    npm run lint
    npm run typecheck

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

@luisfelipe-alt
luisfelipe-alt requested a review from a team as a code owner September 26, 2026 21:46
@github-actions github-actions Bot added the size/l A large sized PR label Sep 26, 2026
@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

📊 PR Size: size/L

  • Lines changed: 628
  • Additions: +542
  • Deletions: -86
  • Files changed: 7

@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 Gemini CLI user interface. By refining how the terminal handles scroll positions, layout height budgets, and keyboard navigation, it ensures that users can interact with tools and review conversation history without the viewport unexpectedly resetting or collapsing.

Highlights

  • Scroll Preservation: Updated the virtualized list to only auto-scroll to the bottom when the user is already pinned to the bottom, preventing unexpected jumps when reviewing history.
  • Layout Budgeting: Implemented a height partitioning strategy in the standard buffer to allocate space between pending history items and the tool confirmation queue, preventing UI overflow.
  • Navigation Stability: Restricted unconstrained height mode toggling so that standard navigation keys no longer trigger layout collapses, reserving this behavior for explicit commands.
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-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 🔒 maintainer only ⛔ Do not contribute. Internal roadmap item. labels Sep 26, 2026

@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 introduces dynamic height partitioning between pending history items and the tool confirmation queue in standard buffer mode, improves scroll position preservation, and ensures unconstrained height only collapses on Escape or 'show more lines' commands. The review feedback highlights a potential performance regression where the entire pendingItems element tree and virtualized list are re-created on every render due to the highly dynamic uiState object in the dependency array. To resolve this, the reviewer suggests extracting the UIStateContext.Provider wrapper into a separate helper component to keep the dependency array stable.

Comment thread packages/cli/src/ui/components/MainContent.tsx
Comment thread packages/cli/src/ui/components/MainContent.tsx
- Guard scrollToEnd() in MainContent when confirmingTool updates so it only auto-scrolls if the virtualized list is already pinned to the bottom.
    - Partition availableTerminalHeight between visible pendingHistoryItems (40%) and ToolConfirmationQueue (60%) in standard buffer mode to keep combined dynamic output within terminal rows.
    - Restrict unconstrained height auto-collapse in AppContainer.handleGlobalKeypress to explicit SHOW_MORE_LINES and ESCAPE commands instead of arbitrary keypresses.
@luisfelipe-alt
luisfelipe-alt force-pushed the bugfix/WT-engineer_561555609 branch from 05bc634 to 2870eb7 Compare September 30, 2026 19:58
@luisfelipe-alt

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 introduces dynamic height partitioning and scroll position preservation in the CLI's MainContent component, preventing terminal overflows in standard buffer mode when tool confirmations are active. It also refactors key matching in AppContainer to only collapse unconstrained height on Escape or show-more-lines keys. A critical issue was identified in the height partitioning calculation: because perPendingItemHeight has a minimum constraint of 4, the actual total height allocated to pending items can exceed rawPendingBudget, which can still cause terminal overflows. It is recommended to calculate confirmationQueueBudget using the actual allocated pending height instead.

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

Comment thread packages/cli/src/ui/components/MainContent.tsx
@luisfelipe-alt
luisfelipe-alt force-pushed the bugfix/WT-engineer_561555609 branch from 9ff502c to 6c1d608 Compare September 30, 2026 20:48
@luisfelipe-alt

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 introduces dynamic height partitioning and scroll position preservation in the terminal UI. Specifically, it ensures that the terminal does not collapse unconstrained height on navigation keys (only on Escape), checks if the user is at the bottom of the scrollable list before auto-scrolling on tool confirmation, and partitions the available terminal height between pending history items and the tool confirmation queue in standard buffer mode. Feedback was provided regarding the minimum height constraints used during partitioning; enforcing a hard minimum of 4 for pending items and 6 for the confirmation queue can still cause overflows on very small terminals (height < 10), so it is recommended to scale these minimums down to 1 when partitioning.

Comment thread packages/cli/src/ui/components/MainContent.tsx Outdated
@luisfelipe-alt
luisfelipe-alt force-pushed the bugfix/WT-engineer_561555609 branch from d3f5074 to 993c4a5 Compare September 30, 2026 21:54
@luisfelipe-alt

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 introduces dynamic height partitioning and scroll position preservation in the terminal UI's MainContent component to prevent terminal overflows and improve the user experience when tool confirmations arrive. It also updates AppContainer to collapse the unconstrained height view when pressing Escape or the show-more-lines key, backed by new unit tests. The reviewer feedback correctly points out that when collapsing this view, the keypress event is not marked as handled, which can cause event leakage and unintended side effects in other components.

Comment thread packages/cli/src/ui/AppContainer.tsx
@luisfelipe-alt
luisfelipe-alt force-pushed the bugfix/WT-engineer_561555609 branch from 1b2531d to cb55d76 Compare September 30, 2026 22:32
@luisfelipe-alt

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 implements dynamic height partitioning between pending history items and the tool confirmation queue in standard buffer mode to prevent terminal overflows, preserves scroll position when tool confirmations arrive, and allows collapsing the expanded view using the Escape key. Feedback highlights two key issues: first, height partitioning should be bypassed in alternate buffer mode to avoid unnecessary truncation of scrollable items; second, the Escape key should only collapse the expanded view when no dialogs are open to prevent swallowing keypresses intended for closing dialogs.

Comment thread packages/cli/src/ui/components/MainContent.tsx Outdated
Comment thread packages/cli/src/ui/AppContainer.tsx
@luisfelipe-alt
luisfelipe-alt force-pushed the bugfix/WT-engineer_561555609 branch from 9329da1 to d6025a9 Compare September 30, 2026 23:01
@luisfelipe-alt

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 introduces scroll position preservation and dynamic height partitioning in MainContent to prevent terminal overflows, along with logic in AppContainer to handle Escape key collapses only when no dialogs are open. Feedback focuses on refactoring the fragile list of 19 individual dialog state checks in AppContainer by memoizing and reusing dialogsVisible to simplify the keypress handler and its dependency array. Additionally, a potential terminal overflow bug was identified in MainContent's height budget calculation when multiple pending items are present, with a recommendation to use a more precise distribution method like the Largest Remainder Method.

Comment thread packages/cli/src/ui/AppContainer.tsx
Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
Comment thread packages/cli/src/ui/components/MainContent.tsx Outdated
@luisfelipe-alt
luisfelipe-alt force-pushed the bugfix/WT-engineer_561555609 branch from a4bc489 to f73ddce Compare September 30, 2026 23:49
@luisfelipe-alt

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 introduces scroll position preservation and dynamic height partitioning in the CLI's main content view to prevent terminal overflows. Specifically, it updates 'MainContent' to only scroll to the end on new tool confirmations if the user is already at the bottom, and partitions the available terminal height budget between pending history items and the tool confirmation queue when in standard buffer mode. Additionally, it refactors 'AppContainer' to ensure that pressing Escape only collapses the unconstrained height when no dialogs are visible, and adds comprehensive unit tests to verify these behaviors. There are no review comments to address, and I have no additional feedback to provide.

@DavidAPierce

Copy link
Copy Markdown
Contributor

Findings

🔴 Critical: Scroll Preservation Guard is Bypassed in Production by Composer.tsx

While the check in packages/cli/src/ui/components/MainContent.tsx correctly checks whether the user is at the bottom:

useEffect(() => {
  if (showConfirmationQueue && scrollableListRef.current) {
    const scrollState = scrollableListRef.current.getScrollState?.();
    const isAtBottom = scrollState
      ? scrollState.scrollHeight -
          scrollState.innerHeight -
          scrollState.scrollTop <=
        1
      : true;
    if (isAtBottom) {
      scrollableListRef.current.scrollToEnd();
    }
  }
}, [showConfirmationQueue, confirmingToolCallId]);

There is an active second path in packages/cli/src/ui/components/Composer.tsx (lines 61–65):

useEffect(() => {
  if (hasPendingActionRequired) {
    appEvents.emit(AppEvent.ScrollToBottom);
  }
}, [hasPendingActionRequired]);

Where hasPendingActionRequired is computed by useComposerStatus (lines 25–39) as:

const hasPendingToolConfirmation = useMemo(
  () =>
    (uiState.pendingHistoryItems ?? [])
      .filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
      .some((item) => item.tools.some((tool) => tool.status === CoreToolCallStatus.AwaitingApproval)),
  [uiState.pendingHistoryItems],
);

const hasPendingActionRequired =
  hasPendingToolConfirmation || ...

And MainContent.tsx listens to AppEvent.ScrollToBottom directly (lines 89–97):

useEffect(() => {
  const handleScroll = () => {
    scrollableListRef.current?.scrollToEnd();
  };
  appEvents.on(AppEvent.ScrollToBottom, handleScroll);
  return () => {
    appEvents.off(AppEvent.ScrollToBottom, handleScroll);
  };
}, []);

Impact:
When Composer and MainContent are mounted together in the app layout (e.g. DefaultAppLayout), any tool confirmation arriving sets hasPendingToolConfirmation = true, which sets hasPendingActionRequired = true. Composer immediately emits AppEvent.ScrollToBottom, and MainContent's handleScroll handler unconditionally calls scrollToEnd(), completely overriding the isAtBottom guard in MainContent.

The unit test in MainContent.test.tsx passed because it renders <MainContent /> in isolation without <Composer />.

Recommendation:
Prevent Composer.tsx from emitting AppEvent.ScrollToBottom when the pending action is a tool confirmation (since MainContent manages its own confirmation queue scroll positioning), or make handleScroll in MainContent also respect isAtBottom.


🟡 Improvements

  1. Dead variable in AppContainer.tsx (enteringConstrainHeightMode):
    In packages/cli/src/ui/AppContainer.tsx (lines 1917–1932):

    let enteringConstrainHeightMode = false;
    if (
      !constrainHeight &&
      (keyMatchers[Command.SHOW_MORE_LINES](key) ||
        (keyMatchers[Command.ESCAPE](key) && !dialogsVisible))
    ) {
      enteringConstrainHeightMode = true;
      setConstrainHeight(true);
      if (keyMatchers[Command.SHOW_MORE_LINES](key)) {
        toggleLastTurnTools();
      }
      if (!isAlternateBuffer) {
        refreshStatic();
      }
      return true;
    }

    Because this block returns return true;, execution never continues down to line 1972 (keyMatchers[Command.SHOW_MORE_LINES](key) && !enteringConstrainHeightMode) when enteringConstrainHeightMode is true. When execution does reach line 1972, enteringConstrainHeightMode is guaranteed to be false.
    Suggestion: Remove let enteringConstrainHeightMode = false; and simplify line 1972 to keyMatchers[Command.SHOW_MORE_LINES](key).

  2. Integration Test Coverage for Composer + MainContent Interaction:
    Add a test that renders both MainContent and Composer (or verifies that AppEvent.ScrollToBottom is not emitted on tool confirmation when scrolled up) to ensure regressions do not re-introduce scroll resets.


@luisfelipe-alt
luisfelipe-alt force-pushed the bugfix/WT-engineer_561555609 branch from 0f907d9 to 27244b2 Compare October 1, 2026 19:16
@luisfelipe-alt

Copy link
Copy Markdown
Contributor Author

Findings

🔴 Critical: Scroll Preservation Guard is Bypassed in Production by Composer.tsx

While the check in packages/cli/src/ui/components/MainContent.tsx correctly checks whether the user is at the bottom:

useEffect(() => {
  if (showConfirmationQueue && scrollableListRef.current) {
    const scrollState = scrollableListRef.current.getScrollState?.();
    const isAtBottom = scrollState
      ? scrollState.scrollHeight -
          scrollState.innerHeight -
          scrollState.scrollTop <=
        1
      : true;
    if (isAtBottom) {
      scrollableListRef.current.scrollToEnd();
    }
  }
}, [showConfirmationQueue, confirmingToolCallId]);

There is an active second path in packages/cli/src/ui/components/Composer.tsx (lines 61–65):

useEffect(() => {
  if (hasPendingActionRequired) {
    appEvents.emit(AppEvent.ScrollToBottom);
  }
}, [hasPendingActionRequired]);

Where hasPendingActionRequired is computed by useComposerStatus (lines 25–39) as:

const hasPendingToolConfirmation = useMemo(
  () =>
    (uiState.pendingHistoryItems ?? [])
      .filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
      .some((item) => item.tools.some((tool) => tool.status === CoreToolCallStatus.AwaitingApproval)),
  [uiState.pendingHistoryItems],
);

const hasPendingActionRequired =
  hasPendingToolConfirmation || ...

And MainContent.tsx listens to AppEvent.ScrollToBottom directly (lines 89–97):

useEffect(() => {
  const handleScroll = () => {
    scrollableListRef.current?.scrollToEnd();
  };
  appEvents.on(AppEvent.ScrollToBottom, handleScroll);
  return () => {
    appEvents.off(AppEvent.ScrollToBottom, handleScroll);
  };
}, []);

Impact: When Composer and MainContent are mounted together in the app layout (e.g. DefaultAppLayout), any tool confirmation arriving sets hasPendingToolConfirmation = true, which sets hasPendingActionRequired = true. Composer immediately emits AppEvent.ScrollToBottom, and MainContent's handleScroll handler unconditionally calls scrollToEnd(), completely overriding the isAtBottom guard in MainContent.

The unit test in MainContent.test.tsx passed because it renders <MainContent /> in isolation without <Composer />.

Recommendation: Prevent Composer.tsx from emitting AppEvent.ScrollToBottom when the pending action is a tool confirmation (since MainContent manages its own confirmation queue scroll positioning), or make handleScroll in MainContent also respect isAtBottom.

🟡 Improvements

  1. Dead variable in AppContainer.tsx (enteringConstrainHeightMode):
    In packages/cli/src/ui/AppContainer.tsx (lines 1917–1932):

    let enteringConstrainHeightMode = false;
    if (
      !constrainHeight &&
      (keyMatchers[Command.SHOW_MORE_LINES](key) ||
        (keyMatchers[Command.ESCAPE](key) && !dialogsVisible))
    ) {
      enteringConstrainHeightMode = true;
      setConstrainHeight(true);
      if (keyMatchers[Command.SHOW_MORE_LINES](key)) {
        toggleLastTurnTools();
      }
      if (!isAlternateBuffer) {
        refreshStatic();
      }
      return true;
    }

    Because this block returns return true;, execution never continues down to line 1972 (keyMatchers[Command.SHOW_MORE_LINES](key) && !enteringConstrainHeightMode) when enteringConstrainHeightMode is true. When execution does reach line 1972, enteringConstrainHeightMode is guaranteed to be false.
    Suggestion: Remove let enteringConstrainHeightMode = false; and simplify line 1972 to keyMatchers[Command.SHOW_MORE_LINES](key).

  2. Integration Test Coverage for Composer + MainContent Interaction:
    Add a test that renders both MainContent and Composer (or verifies that AppEvent.ScrollToBottom is not emitted on tool confirmation when scrolled up) to ensure regressions do not re-introduce scroll resets.

Done.

addressed

@luisfelipe-alt

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 improves terminal height management, scroll behavior, and key handling in the CLI UI. Key changes include preventing unconstrained height collapse on navigation keys, refining scroll-to-bottom behavior to avoid scrolling when a tool confirmation is pending or when the user has scrolled up, and implementing dynamic height partitioning between pending history items and the tool confirmation queue in standard buffer mode to prevent terminal overflows. Comprehensive unit tests have been added to verify these changes. I have no additional feedback to provide as no review comments were submitted.

@DavidAPierce
DavidAPierce enabled auto-merge October 1, 2026 19:48
@DavidAPierce
DavidAPierce added this pull request to the merge queue Oct 1, 2026
Merged via the queue into google-gemini:main with commit c9096a8 Oct 1, 2026
33 checks passed
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 🔒 maintainer only ⛔ Do not contribute. Internal roadmap item. priority/p1 Important and should be addressed in the near term. priority/p2 Important but can be addressed in a future release. size/l A large sized PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scroll position jumps to top on new message arrival

2 participants