Skip to content

Centralize Key Binding Logic and Refactor (Reopen)#5356

Merged
jacob314 merged 10 commits intogoogle-gemini:mainfrom
Lee-WonJun:feature/issue-1950-stage-1-keymatcher
Aug 9, 2025
Merged

Centralize Key Binding Logic and Refactor (Reopen)#5356
jacob314 merged 10 commits intogoogle-gemini:mainfrom
Lee-WonJun:feature/issue-1950-stage-1-keymatcher

Conversation

@Lee-WonJun
Copy link
Contributor

TLDR

This PR reopens the work from the #1969 previously closed PR. For implementation details, please refer to the previous discussion.

  1. App.tsx Now Uses the Key Interface

    • Similar to InputPrompt.ts, App.tsx has been updated to use the Key interface by replacing the hook.
    • From what I can tell, the existing test cases still pass, and I've verified through debug logs that backward compatibility does not appear to be broken.
    • However, since I’m mainly a backend developer, I don’t have deep insight into the importance or prioritization of React hook patterns.
    • If the previous lower-level hook used in App.tsx was intentional, then maybe keyBindings needs to be revised to accommodate it—or perhaps it should leverage the same type + mapper structure used here. But honestly, that approach doesn't feel very clean to me. I think it's better to follow a consistent pattern across the app.
  2. Context on Centralization Effort

    • As noted in the original Add custom SHORTCUT key binding with Documentation #1950 issue, the goal of centralizing key bindings is to lay the groundwork for future support for user-configurable key mappings.
    • Even without that functionality, I believe this centralization improves maintainability and consistency, making the codebase cleaner and easier to evolve.
    • Regardless of whether this PR gets merged, I hope the idea of giving users more control over configuration settings remains part of the long-term roadmap (as it seems to already be tracked under the epic).
    • As with the previous PR, this is not intended to impose a single solution—if the original structure (such as placing bindings directly in terminal-style components) was chosen deliberately, I’d appreciate your thoughts or guidance.

Reviewer Test Plan

Confirm that all previous key bindings still work as expected.

Testing Matrix

🍏 🪟 🐧
npm run O
npx
Docker
Podman - -
Seatbelt - -

Linked issues / bugs

#1950 #1969

Thanks for the helpful feedback, @mattKorwel

@Lee-WonJun Lee-WonJun requested a review from a team as a code owner August 1, 2025 15:15
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.

Summary of Changes

Hello @Lee-WonJun, 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 reopens previous work to centralize the application's key binding logic. It introduces a dedicated module for defining key combination matchers and refactors both the main App.tsx and InputPrompt.tsx components to consume these centralized definitions. The primary goal is to enhance code maintainability and consistency, while also preparing the codebase for future user-customizable keybindings.

Highlights

  • Centralized Key Binding Logic: Introduced a new keyBindings.ts file that centralizes the definitions of various key combinations (e.g., Ctrl+C, Ctrl+D, arrow keys, etc.) into reusable matcher functions. This aims to improve consistency and maintainability across the application's key handling.
  • Refactoring of App.tsx: The main App.tsx component has been refactored to remove its direct dependency on Ink's useInput hook and InkKeyType. Instead, it now utilizes the custom useKeypress hook and the newly centralized keyMatchers to handle global key events, streamlining its keypress logic.
  • Refactoring of InputPrompt.tsx: The InputPrompt.tsx component has also been updated to leverage the new keyMatchers for its extensive key handling logic, replacing numerous inline conditional checks with calls to the centralized matcher functions. This promotes a consistent pattern for key event processing.
  • Foundation for User-Configurable Key Mappings: This centralization effort lays the groundwork for future features, specifically enabling support for user-configurable key mappings, as noted in the PR description and linked issue #1950.
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 in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

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 issue 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 or fill out our survey to provide feedback.

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 pull request does a great job of centralizing the key binding logic into the new keyBindings.ts file. This significantly improves the readability and maintainability of the key handling logic in both App.tsx and InputPrompt.tsx.

However, there is a critical issue introduced by replacing Ink's useInput hook with the custom useKeypress hook in App.tsx. This change bypasses Ink's focus management system, causing key press handlers in both App.tsx and InputPrompt.tsx to be active at the same time. This leads to conflicting behavior, for instance with Ctrl+C, which now both clears the input and attempts to quit the application. I've left a comment with a suggested fix for this.

@mattKorwel
Copy link
Collaborator

@Lee-WonJun thanks for resurrecting this! Can you address the comment from the menu box about the overlapping keys?

@jacob314 this one looks good to me. I want to double check with you.

@Lee-WonJun
Copy link
Contributor Author

Lee-WonJun commented Aug 1, 2025

Does my understanding of the Gemini review seem correct?

  • Previously, either useInput or useKeypress would run — meaning, if useKeypress was active, the useInput hook would not be triggered. Is that the intended meaning?

I'm not fully familiar with how priority or exclusivity works between them, but if my assumption is correct, it seems that both hooks were already running concurrently even before.

When I test it with the stable version of Gemini or the main branch build (while both components are active), all hooks appear to be invoked. So perhaps I misunderstood the intent, or maybe there's some focus management behavior that I'm not aware of?

33.mp4

(Please note that I may be running into an XY problem due to my limited understanding of Ink or frontend behavior.)

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a great step in the right direction. However lets tweak this so it can be more data driven and less imperative code .

That means create an enum of all the Commands to assign keyboard shortcuts to
and for each one provide a list of matching keys where each key is

{
key | String,
ctrl | boolean,
shift | boolean,
alt | boolean,
command | boolean,
}

I would expect Gemini CLI should be able to one short refactor from this imperative code to that data driven code. Please let me know if you have any questions.

The reason that is important is it will make this code then scale to allowing users to provide their own keybinding mappings.

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'll approve this to land once the changes requested are made and there is some tests for the new keyBindings functionality.

Copy link
Contributor

Choose a reason for hiding this comment

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

note that text-buffer.ts also has a large number of key bindings that should be moved to use the new system over time.

@Lee-WonJun Lee-WonJun force-pushed the feature/issue-1950-stage-1-keymatcher branch from 8dbbb09 to 4fbf92f Compare August 2, 2025 04:58
@Lee-WonJun
Copy link
Contributor Author

@jacob314 thanks for your feedback and suggestions! I've clarified my thought process below based on your comments.

Reason for Using Lambda Functions

The main reason I've implemented this using lambda functions is that, I'm not aware of the detailed plans for all the shortcuts (or key binding) that gemini-cli will need to support.


Ideal Architecture and Staged Implementation

I'm also a big fan of data-driven DSLs (Domain-Specific Languages), and the ideal architecture I had in mind is as follows:
[String DSL (User Config)][ADT-based DSL (Core)][Callable Interface]

However, this PR follows a staged approach rather than implementing the entire feature at once. In this first stage (Stage 1), I've focused on consolidating the interfaces related to shortcuts.

I've excluded text-buffer.ts for now as it seems to be a more low-level component, which I'll explain further below.


Thoughts on DSL Design

Users will need to be able to override shortcuts via settings.json. Therefore, a simple string-based JSON DSL would be the most intuitive for user configuration:

{
  "keymap": {
    "command": ["ctrl+A+B", "ctrl+A+ALT"]
  }
}

The system can't interpret this string directly, so it needs to be converted into an internal data structure.

For example, to support complex key combinations with AND/OR logic, we could design the DSL with a recursive ADT (Abstract Data Type) structure. This would be very fluent and expressive, but it also makes the DSL structure itself more complex to write.

Command: [
  {
    op: AND,
    keys: ['A', 'CTRL', { op: OR, keys: ['B', 'ALT'] }] // more combination
  }
]

Conversely, if we only allow simple shortcuts, a structure like the one you suggested would also be great:

Command: [
  { key: 'A', clt: true },
  { key: 'a', clt: true }
]

However, since I don't know the full scope of user configurations (Case-sensitivity, multi input , and so on) that gemini-cli will eventually allow, I felt that defining such a complex DSL at this stage would be an overspec.


Role of the Matcher and Implementation Direction

If we have a data-value-only DSL, we need a Decision Interpreter to parse it and determine the final action. I decided that representing this interpreter's interface using a Map would be more declarative and extensible. (Though it doesn't have to be a Map—a single function or another interface could work, but a Map seemed like the best fit.)

Therefore, the Matcher I've written represents the final form after the DSL has been interpreted. This means if a DSL existed, we could dynamically create the Matcher at boot-up by reading the DSL. Since the upstream DSL hasn't been defined yet, it currently only contains hard-coded lambdas.

The reason I didn't include text-buffer.ts in the Matcher implementation is that, as the name implies, it's about the buffer and handles more low-level manipulation. This role is different from the Matcher, which is closer to a 'key' interface. (This is my impression from looking at the code).

In the future, if an ADT-based DSL is implemented in the core, I expect text-buffer.ts would use this DSL data as config constants instead of magic numbers. For this reason, I intentionally excluded it from the higher-level Matcher implementation.


My understanding is that your suggestions were mainly about moving towards a data-driven DSL. I hope my explanation of the current approach and future direction makes sense in that context. Is my understanding correct?

Please feel free to share any more thoughts you have. I'd appreciate the feedback!

@Lee-WonJun Lee-WonJun force-pushed the feature/issue-1950-stage-1-keymatcher branch from 937b9a2 to f31ed08 Compare August 4, 2025 13:25
@Lee-WonJun
Copy link
Contributor Author

Lee-WonJun commented Aug 4, 2025

@jacob314 I've made some code changes to support a more data-driven config. I’d love to hear your thoughts on whether this is heading in a positive direction.


  1. KeyBinding types and default bindings have been moved into the config.
    In the future, I hope to either parse these from a user-level config or use them directly in the settings interface.

    For now, the KeyBinding behavior remains almost identical to the existing ui/Key implementation. This is intentional to maintain backward compatibility — for instance, { sequence: '\x18', ctrl: true } should still work the same way. I deliberately avoided introducing a completely new type or breaking compatibility in this PR.

  2. The Matcher dynamically generates an interface that reads the binding structure and evaluates whether it matches a specific command enum.

  3. Some important caveats:

    • 3-1) After my initial changes, reverse search functionality was added, which led to the introduction of new enums. In these cases, the command differs slightly — for example, SUBMIT_REVERSE_SEARCH vs. SUBMIT. Although they are similar, their behavior diverges, so keeping them separate makes sense. However, I’m not yet sure what the best naming convention should be, and it’s also unclear how much of this will eventually be user-overridable. We’ll need to clarify this at some point.

    • 3-2) Currently, multi-key shortcuts (e.g., Ctrl + A + B) are not supported.

    • 3-3) As expected, since the config does not define a matching priority, a command that matches first may overshadow lower-priority commands.

@Lee-WonJun Lee-WonJun requested a review from jacob314 August 4, 2025 15:31
@Lee-WonJun
Copy link
Contributor Author

/gemini review

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 an excellent job of centralizing the key binding logic into a new data-driven configuration. The refactoring in App.tsx and InputPrompt.tsx to use the new keyMatchers greatly improves code clarity and maintainability, and paves the way for user-configurable keybindings. The addition of comprehensive tests for the new key binding and matching logic is also a great addition.

I've found one issue related to how key bindings for Ctrl+Shift combinations are defined, which appears to be based on a misunderstanding of how readline reports key presses. The fix is straightforward and will make the new configuration more correct and less confusing.

Comment on lines 160 to 168
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The key bindings for QUIT and EXIT include entries for uppercase characters (e.g., { key: 'C', ctrl: true }). This seems to be based on a misunderstanding of how key presses are reported. The useKeypress hook, relying on Node.js's readline, typically reports key names in lowercase, using the shift flag to indicate an uppercase character (e.g., Ctrl+Shift+C would be { name: 'c', ctrl: true, shift: true }).

The existing binding { key: 'c', ctrl: true } already correctly handles both Ctrl+C and Ctrl+Shift+C because the shift modifier is not specified in the binding, so it's ignored during matching.

The bindings for uppercase keys are redundant and misleading. They should be removed for clarity and correctness.

  [Command.QUIT]: [{ key: 'c', ctrl: true }],
  // Original: key.ctrl && (key.name === 'd' || key.name === 'D')
  [Command.EXIT]: [{ key: 'd', ctrl: true }],

Copy link
Contributor

Choose a reason for hiding this comment

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

generally agree with this. suspect previous code we had was confused but it would be good to verify this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At least in my environment, it works after removing the uppercase binding. If case sensitivity is needed, we could probably use the sequence instead.

@jacob314
Copy link
Contributor

jacob314 commented Aug 8, 2025

Please let me know when you are ready for me to review again. Can't wait to have custom keyboard shortcuts.

@Lee-WonJun Lee-WonJun force-pushed the feature/issue-1950-stage-1-keymatcher branch 2 times, most recently from d9bf574 to e92d210 Compare August 8, 2025 06:14
@Lee-WonJun
Copy link
Contributor Author

Lee-WonJun commented Aug 8, 2025

@jacob314 I’ve resolved the conflicting parts.

@Lee-WonJun Lee-WonJun force-pushed the feature/issue-1950-stage-1-keymatcher branch 2 times, most recently from a7d1596 to e1063d2 Compare August 9, 2025 03:21
@Lee-WonJun Lee-WonJun force-pushed the feature/issue-1950-stage-1-keymatcher branch from e1063d2 to 4a0abd1 Compare August 9, 2025 04:02
@Lee-WonJun
Copy link
Contributor Author

By changing the hook in app.ts, conflicts occur whenever app.ts is updated due to changes such as depth. I fix it every time, but I can’t always keep my branch in sync with the main branch.

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.

Minor comments. Attempting to approve so you can merge to unblock.

Comment on lines 160 to 168
Copy link
Contributor

Choose a reason for hiding this comment

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

generally agree with this. suspect previous code we had was confused but it would be good to verify this.

Copy link
Contributor

Choose a reason for hiding this comment

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

this should be a separate command
COMPLETION_UP
that matches both of these keys

that way users who are annoyed that the up arrow navigates through completion history can change it without breaking other things coupled to NAVIGATION_UP

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree

Copy link
Contributor

Choose a reason for hiding this comment

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

make this COMPLETION_DOWN

@jacob314 jacob314 enabled auto-merge August 9, 2025 07:00
@jacob314 jacob314 added this pull request to the merge queue Aug 9, 2025
Merged via the queue into google-gemini:main with commit b8084ba Aug 9, 2025
14 checks passed
thacio added a commit to thacio/auditaria that referenced this pull request Aug 9, 2025
acoliver referenced this pull request in vybestack/llxprt-code Aug 9, 2025
github-merge-queue bot pushed a commit that referenced this pull request Aug 9, 2025
acoliver referenced this pull request in vybestack/llxprt-code Aug 10, 2025
thacio added a commit to thacio/auditaria that referenced this pull request Aug 11, 2025
JeongJaeSoon pushed a commit to JeongJaeSoon/gemini-cli that referenced this pull request Aug 21, 2025
JeongJaeSoon pushed a commit to JeongJaeSoon/gemini-cli that referenced this pull request Aug 21, 2025
involvex pushed a commit to involvex/gemini-cli that referenced this pull request Sep 11, 2025
involvex pushed a commit to involvex/gemini-cli that referenced this pull request Sep 11, 2025
reconsumeralization pushed a commit to reconsumeralization/gemini-cli that referenced this pull request Sep 19, 2025
reconsumeralization pushed a commit to reconsumeralization/gemini-cli that referenced this pull request Sep 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants