Skip to content

fix(cli): improve focus navigation for interactive and background shells#18343

Merged
galz10 merged 6 commits intomainfrom
galzahavi/fix/shell-keybinding
Feb 6, 2026
Merged

fix(cli): improve focus navigation for interactive and background shells#18343
galz10 merged 6 commits intomainfrom
galzahavi/fix/shell-keybinding

Conversation

@galz10
Copy link
Collaborator

@galz10 galz10 commented Feb 4, 2026

Summary

Improve shell focus navigation and global toggle bubbling in the CLI. This PR enables Tab to focus the interactive shell from message history, Shift+Tab to unfocus/escape the shell, and ensures Ctrl+B correctly toggles the background shell display by allowing events to bubble out of specialized UI components.

Details

  • Navigation Enhancements: Updated keyBindings.ts to explicitly define Shift+Tab for UNFOCUS_SHELL_INPUT, allowing for a dedicated "escape hatch" from focused shells.
  • Global Focus Logic: Refactored AppContainer.tsx to handle Tab-to-focus and Shift+Tab-to-unfocus correctly. The logic now allows Tab to bubble through to the shell (unblocking auto-completion) while still showing the relevant UI hints.
  • Component-Level Focus: Integrated useKeypress into ShellToolMessage.tsx to allow users to focus a running shell directly from the history using Tab.
  • Toggle Reliability: Fixed Ctrl+B (TOGGLE_BACKGROUND_SHELL) bubbling in InputPrompt.tsx and BackgroundShellDisplay.tsx. This ensures the global toggle always reaches AppContainer regardless of which component currently has priority.
  • UI UX Improvements: Updated the focus hint in ToolShared.tsx to explicitly mention Shift+Tab for unfocusing, improving discoverability.

Related Issues

How to Validate

  1. Run a tool that opens an interactive shell (e.g., !vim).
  2. Verify that pressing Tab while the shell is focused still performs its shell-native function (e.g., in vim).
  3. Verify that pressing Shift+Tab while the shell is focused escapes focus and returns focus to the Gemini prompt.
  4. From the Gemini prompt (with a running shell), press Tab to return focus to the shell.
  5. Press Ctrl+B to background the shell.
  6. Press Ctrl+B again to open the background shell display.
  7. While in the background shell display, press Ctrl+B to close it.
  8. Run tests: npx vitest run packages/cli/src/ui/AppContainer.test.tsx packages/cli/src/ui/components/InputPrompt.test.tsx packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx

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

- Update key bindings to explicitly support Shift+Tab for unfocusing shells.
- Implement Tab-to-focus and Shift+Tab-to-unfocus in AppContainer for both interactive and background shell displays.
- Allow Tab to bubble to the focused shell (unblocking auto-completion) while still displaying the unfocus hint.
- Enable Tab focus directly from ShellToolMessage components via useKeypress.
- Ensure Ctrl+B correctly toggles (opens and closes) the BackgroundShellDisplay by allowing it to bubble from the display component.
- Add unit tests for the new focus/unfocus behavior in AppContainer.
@galz10 galz10 requested review from a team as code owners February 4, 2026 21:24
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @galz10, 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 refines the user experience for interacting with interactive and background shells within the CLI. It introduces more intuitive keyboard navigation for focusing and unfocusing shells and resolves issues where global commands, such as toggling the background shell, were not reliably propagating through the UI. These changes aim to make shell interactions smoother and more predictable for users.

Highlights

  • Improved Shell Focus Navigation: Users can now use 'Tab' to focus an interactive shell from message history and 'Shift+Tab' to unfocus/escape the shell, providing a dedicated escape mechanism.
  • Enhanced Global Toggle Reliability: The 'Ctrl+B' command for toggling the background shell display now correctly bubbles through specialized UI components, ensuring consistent functionality regardless of the current focus.
  • Updated Keybinding Configuration: The keybinding for unfocusing the shell input has been explicitly set to 'Shift+Tab' in the configuration.
  • UI/UX Clarity: The focus hint displayed in the UI has been updated to explicitly mention 'Shift+Tab' for unfocusing, improving discoverability for users.
  • Comprehensive Test Coverage: New tests have been added for AppContainer.tsx, InputPrompt.test.tsx, and ShellToolMessage.test.tsx to validate the new focus and unfocus behaviors.
Changelog
  • docs/cli/keyboard-shortcuts.md
    • Updated the documentation to reflect that 'Shift + Tab' is now used to focus the Gemini input from the shell input.
  • package-lock.json
    • Added 'peer: true' flag to several dependency entries, indicating they are peer dependencies.
  • packages/cli/src/config/keyBindings.ts
    • Modified the UNFOCUS_SHELL_INPUT command to explicitly require the 'shift' key in combination with 'tab'.
  • packages/cli/src/ui/AppContainer.test.tsx
    • Added new test cases to verify that 'Tab' correctly focuses the shell input and 'Shift+Tab' correctly unfocuses it.
  • packages/cli/src/ui/AppContainer.tsx
    • Refactored keypress handling for FOCUS_SHELL_INPUT to allow events to bubble when the shell is already focused.
    • Introduced explicit handling for UNFOCUS_SHELL_INPUT to set embeddedShellFocused to false and consume the event.
    • Changed TOGGLE_BACKGROUND_SHELL handling to return false, allowing the event to bubble up to higher-level components.
  • packages/cli/src/ui/components/BackgroundShellDisplay.tsx
    • Modified the keypress handler for TOGGLE_BACKGROUND_SHELL to return false, ensuring the event bubbles up.
  • packages/cli/src/ui/components/InputPrompt.tsx
    • Added a keypress handler for TOGGLE_BACKGROUND_SHELL to return false, allowing the event to bubble up.
    • Changed the keypress handler for FOCUS_SHELL_INPUT to return false, allowing the event to bubble up if not handled by autocomplete.
  • packages/cli/src/ui/components/ShellInputPrompt.tsx
    • Added a keypress handler for UNFOCUS_SHELL_INPUT to return false, allowing the event to bubble up.
  • packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx
    • Updated the test assertion for the shell focus hint to expect '(Shift+Tab to unfocus)' instead of '(Focused)'.
  • packages/cli/src/ui/components/messages/ShellToolMessage.tsx
    • Imported useKeypress, Command, and keyMatchers.
    • Implemented the useKeypress hook to enable focusing a shell directly from message history using the 'Tab' key.
  • packages/cli/src/ui/components/messages/ToolShared.tsx
    • Updated the FocusHint component to display '(Shift+Tab to unfocus)' when the shell is focused, and '(tab to focus)' otherwise.
Activity
  • The author updated relevant documentation, specifically docs/cli/keyboard-shortcuts.md.
  • The author added and updated tests for AppContainer.tsx, InputPrompt.test.tsx, and ShellToolMessage.test.tsx.
  • The author validated the changes on MacOS.
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.

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

Size Change: -5.17 kB (-0.02%)

Total Size: 23.8 MB

Filename Size Change
./bundle/gemini.js 23.8 MB -5.17 kB (-0.02%)
ℹ️ View Unchanged
Filename Size
./bundle/sandbox-macos-permissive-closed.sb 1.03 kB
./bundle/sandbox-macos-permissive-open.sb 890 B
./bundle/sandbox-macos-permissive-proxied.sb 1.31 kB
./bundle/sandbox-macos-restrictive-closed.sb 3.29 kB
./bundle/sandbox-macos-restrictive-open.sb 3.36 kB
./bundle/sandbox-macos-restrictive-proxied.sb 3.56 kB

compressed-size-action

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 pull request does a great job of refactoring the shell focus navigation to be more robust and centralized in AppContainer. The changes to allow key events to bubble up from child components are well-implemented and improve the overall architecture. I've found one issue where a race condition can cause an incorrect warning to be displayed to the user immediately after focusing the shell. The fix is to remove a redundant state update from a child component, reinforcing the principle of consistent UI behavior across components. Otherwise, the changes look solid and the updated tests and documentation are appreciated.

setEmbeddedShellFocused(true);
}
return true;
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Returning false here is correct for allowing the event to bubble up to AppContainer. However, the setEmbeddedShellFocused(true) call on line 937 causes a problem. When the event bubbles, AppContainer sees that embeddedShellFocused is already true and incorrectly displays a warning: "Press Shift+Tab to focus out."

The InputPrompt should not be setting this state. Please remove the if block on lines 933-938 that calls setEmbeddedShellFocused to make AppContainer the single source of truth for this logic.

References
  1. Maintain consistency with existing UI behavior across components. Defer non-standard UX pattern improvements to be addressed holistically rather than in a single component.

@gemini-cli gemini-cli bot added the status/need-issue Pull requests that need to have an associated issue. label Feb 4, 2026
@jacob314
Copy link
Contributor

jacob314 commented Feb 5, 2026

This review was generated by /review-frontend and audited by Jacob.

Overall, the focus navigation improvements and test coverage look great. However, there are a few React-specific issues to address before merging:

  1. Timeout Cleanup Bug in AppContainer.tsx:
    In the useEffect handling app events (around line 1307), warningTimeoutRef.current and tabFocusTimeoutRef.current are captured into local variables (warningTimeout, tabFocusTimeout) outside the cleanup function. This means the cleanup function will only ever clear the timeouts that existed at the exact moment the effect was set up (which might be null). If a timeout is set later via a ref mutation, it will not be cleared on unmount. Please revert to accessing .current directly inside the return () => { ... } block.

  2. Missing setEmbeddedShellFocused in AppContainer.tsx dependencies:
    In AppContainer.tsx around line 1211, setEmbeddedShellFocused is omitted from the useEffect dependency array. Even though it's likely a stable function from context, it must be included to satisfy the react-hooks/exhaustive-deps lint rule.

  3. Cascading State Updates in useEffect (AppContainer.tsx and ShellToolMessage.tsx):
    There are new useEffect blocks that check conditions and then immediately call setEmbeddedShellFocused(false). Because embeddedShellFocused is included in the dependency arrays, this triggers an additional render cycle after the initial render. While this might function correctly in this specific instance, cascading state updates inside useEffect are a React anti-pattern and can sometimes lead to hard-to-debug infinite loops or performance issues. Please consider if this state synchronization can be handled directly within the relevant event handlers (e.g., when a shell exits or is backgrounded) rather than reacting to the state change in an effect.

Everything else (keybinding utils, snapshot updates, and Tab/Shift+Tab behavior) looks solid!

@jacob314
Copy link
Contributor

jacob314 commented Feb 5, 2026

This review was generated by the /review-frontend agent and audited by Jacob.

Overall, this is a solid PR that centralizes focus navigation and fixes several bubbling issues. The extraction of keybinding formatting into keybindingUtils.ts is a nice cleanup.

There are a few React-specific issues that need to be addressed before merging:

1. Stale Timeout Refs in AppContainer.tsx

In AppContainer.tsx, the cleanup function for the appEvents effect was changed to capture the .current values of warningTimeoutRef and tabFocusTimeoutRef at the time the effect mounts:

    const warningTimeout = warningTimeoutRef.current;
    const tabFocusTimeout = tabFocusTimeoutRef.current;
    return () => {
      appEvents.off(AppEvent.PasteTimeout, handlePasteTimeout);
      if (warningTimeout) {
        clearTimeout(warningTimeout);
      }
      if (tabFocusTimeout) {
        clearTimeout(tabFocusTimeout);
      }
    };

Because these refs are used to store timeout IDs that are created/mutated over time, capturing them into local variables during effect execution means the cleanup function will clear the timeout that existed when the component mounted (which is likely null), rather than the currently active timeout when the component unmounts. You should revert to reading ref.current directly inside the cleanup function:

    return () => {
      appEvents.off(AppEvent.PasteTimeout, handlePasteTimeout);
      if (warningTimeoutRef.current) {
        clearTimeout(warningTimeoutRef.current);
      }
      if (tabFocusTimeoutRef.current) {
        clearTimeout(tabFocusTimeoutRef.current);
      }
    };

2. Redundant State Update inside useEffect (ShellToolMessage.tsx)

I agree with the gemini-code-assist review regarding the useEffect in ShellToolMessage.tsx:

  React.useEffect(() => {
    if (
      !isThisShellFocused &&
      ptyId === activeShellPtyId &&
      embeddedShellFocused
    ) {
      setEmbeddedShellFocused(false);
    }
  }, [ /* dependencies */ ]);

This can cause a race condition since it clears the focus state globally from a child component. Now that AppContainer.tsx has a central useEffect to clear embeddedShellFocused when activePtyId is cleared, this effect in ShellToolMessage is redundant and should be removed. Managing this global state centrally in AppContainer prevents conflicting state updates from unmounting or rerendering child components.

3. Testing best practices

The tests use act appropriately for state changes and rely on snapshot/string matching correctly. The test updates in this PR look good and adhere to our guidelines.

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.

I've reviewed the frontend React code. There are a couple of points regarding useEffect dependencies and cleanup functions in ShellToolMessage.tsx and AppContainer.tsx that I've left as separate comments. Otherwise, the logic and focus management improvements look solid.

Review by /review-frontend, audited by Jacob.

- Fix timeout cleanup in AppContainer.tsx by moving it to a dedicated unmount effect, satisfying linter without eslint-disable.
- Remove unused and non-functional tabFocusTimeoutRef from AppContainer.
- Add missing dependencies to useEffect hooks to comply with exhaustive-deps.
- Remove redundant focus-resetting useEffect hooks in AppContainer, useGeminiStream, and ShellToolMessage to eliminate cascading state updates.
- Focus is now managed imperatively within terminal-state handlers.
- Clean up unused imports and test helpers in ShellToolMessage.test.tsx.
| Dismiss background shell list. | `Esc` |
| Move focus from background shell to Gemini. | `Shift + Tab` |
| Move focus from background shell list to Gemini. | `Tab (no Shift)` |
| Show warning when trying to unfocus background shell via Tab. | `Tab (no Shift)` |
Copy link
Contributor

Choose a reason for hiding this comment

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

these are super low value keyboard shortcut documentation messages. I think we need an option to hide them adding to keyBindings and respected by the doc generation script.

// If the shell hasn't produced output in the last 100ms, it's considered idle.
const isIdle = now - lastOutputTimeRef.current >= 100;
if (isIdle && !activePtyId) {

Copy link
Contributor

Choose a reason for hiding this comment

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

As a follow up pr can you add a hook loaded by appContainer that has all the background shell logic. we need to start moving stuff out of AppContainer again.

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

…ackground shells

- Re-introduce `tabFocusTimeoutRef` in `AppContainer` with a 150ms responsive threshold.
- Implement smart-unfocus logic that detects shell activity; automatically unfocuses on Tab if no PTY output is received, otherwise shows a guidance warning.
- Consolidate all shell unfocus handling (`UNFOCUS_SHELL_INPUT`, `UNFOCUS_BACKGROUND_SHELL`, `UNFOCUS_BACKGROUND_SHELL_LIST`) into the `AppContainer` priority keypress handler.
- Remove redundant and conflicting unfocus warning logic from `ShellInputPrompt` and `BackgroundShellDisplay` to prevent double-messaging.
- Update `shellReducer` to track `lastShellOutputTime` for background shells, ensuring consistent activity detection.
- Promote `AppContainer` global keypress handler to high priority to ensure navigation events are intercepted before child component consumption.
- Clean up unused imports and destructured variables in `ShellInputPrompt` and `BackgroundShellDisplay`.
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

@galz10 galz10 enabled auto-merge February 6, 2026 18:23
@galz10 galz10 added this pull request to the merge queue Feb 6, 2026
Merged via the queue into main with commit ec5836c Feb 6, 2026
25 of 26 checks passed
@galz10 galz10 deleted the galzahavi/fix/shell-keybinding branch February 6, 2026 18:48
@galz10
Copy link
Collaborator Author

galz10 commented Feb 6, 2026

/patch preview

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

Patch workflow(s) dispatched successfully!

📋 Details:

  • Channels: preview
  • Commit: ec5836c4d6e6a6aa2be9258859777393099b4796
  • Workflows Created: 1

🔗 Track Progress:

@skeshive
Copy link
Contributor

skeshive commented Feb 6, 2026

/patch preview

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

Patch workflow(s) dispatched successfully!

📋 Details:

  • Channels: preview
  • Commit: ec5836c4d6e6a6aa2be9258859777393099b4796
  • Workflows Created: 1

🔗 Track Progress:

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

🚀 Patch PR Created!

📋 Patch Details:

📝 Next Steps:

  1. Review and approve the hotfix PR: #18472
  2. Once merged, the patch release will automatically trigger
  3. You'll receive updates here when the release completes

🔗 Track Progress:

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

🚀 Patch Release Started!

📋 Release Details:

  • Environment: prod
  • Channel: preview → publishing to npm tag preview
  • Version: v0.28.0-preview.4
  • Hotfix PR: Merged ✅
  • Release Branch: release/v0.28.0-preview.4-pr-18343

⏳ Status: The patch release is now running. You'll receive another update when it completes.

🔗 Track Progress:

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

Patch Release Complete!

📦 Release Details:

🎉 Status: Your patch has been successfully released and published to npm!

📝 What's Available:

🔗 Links:

aswinashok44 pushed a commit to aswinashok44/gemini-cli that referenced this pull request Feb 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status/need-issue Pull requests that need to have an associated issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants