Skip to content

fix(tui): bypass toggleSidebar and toggleMouse global shortcuts when composer is non-empty - #576

Merged
kevincodex1 merged 4 commits into
Gitlawb:mainfrom
euxaristia:fix/tui-cursor-key-interception
Jul 8, 2026
Merged

fix(tui): bypass toggleSidebar and toggleMouse global shortcuts when composer is non-empty#576
kevincodex1 merged 4 commits into
Gitlawb:mainfrom
euxaristia:fix/tui-cursor-key-interception

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Prevents global interception of readline/Emacs cursor movement keybindings (Ctrl+B and Ctrl+E) when typing inside the input composer.

Previously, Ctrl+B (which toggles the sidebar) and Ctrl+E (which toggles mouse mode) were globally intercepted at the model level on keypress. This broke standard Emacs-style readline navigation within the text composer (moving the cursor backward with Ctrl+B and moving it to the end of the line with Ctrl+E).

This fix modifies the key match condition for these global shortcuts so they are bypassed when the input composer contains text. If the composer has text, Ctrl+B and Ctrl+E fall through to the text editor and behave as cursor movement keys. If the composer is empty, they continue to toggle the sidebar and mouse modes as expected.

Changes

internal/tui/model.go

  • Append && m.composerValue() == "" to toggleSidebar (Ctrl+B) and toggleMouse (Ctrl+E) global shortcut switch cases.

internal/tui/keybinding_help_test.go

  • Add TestCtrlBCtrlECursorNavigationBypass to verify both empty-composer toggling and non-empty-composer cursor navigation bypass.

Test plan

  • go test -v ./internal/tui/... — ok

Summary by CodeRabbit

  • Bug Fixes
    • Ctrl+B and Ctrl+E now toggle the sidebar and mouse capture only when the composer is empty and the shortcuts match the default toggle bindings; while typing, they no longer interrupt input.
  • Tests
    • Added coverage confirming correct Ctrl+B/Ctrl+E behavior for both empty and non-empty composer states, without affecting existing toggle-binding expectations.

…composer is non-empty

Allows standard readline/Emacs cursor movement keybindings (Ctrl+B and Ctrl+E) to work inside the prompt composer when typing a prompt. This prevents these keys from being globally intercepted to toggle the sidebar or mouse mode when the composer is populated.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d109cb06-304a-4a5a-bf41-6028a257a5ad

📥 Commits

Reviewing files that changed from the base of the PR and between 27239e9 and d52370a.

📒 Files selected for processing (2)
  • internal/tui/keybindings.go
  • internal/tui/model.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/tui/keybindings.go
  • internal/tui/model.go

Walkthrough

Ctrl+B and Ctrl+E now use a binding-aware empty-composer gate before toggling sidebar and mouse state. Tests cover empty and non-empty composer cases, plus existing remapped and explicit-default chord behavior.

Changes

Ctrl+B/Ctrl+E Composer Gating

Layer / File(s) Summary
Binding gate and toggle handling
internal/tui/keybindings.go, internal/tui/model.go
Default Ctrl+B/Ctrl+E chords are defined as parsed bindings, and updateModel uses requiresEmptyComposer to decide when the toggles must wait for an empty composer before changing mouseReleased or sidebarHidden.
Keybinding behavior tests
internal/tui/keybinding_help_test.go
A new test verifies Ctrl+B and Ctrl+E toggle only with an empty composer, while the existing remapped-binding and explicit-default chord tests remain unchanged.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • Gitlawb/zero#300: Touches the same internal/tui/model.go keypress handling for the Ctrl+B sidebar toggle and related composer-state gating.
  • Gitlawb/zero#417: Modifies the same keybinding resolution path used by Ctrl+B/Ctrl+E handling in keybindings.go and model.go.
  • Gitlawb/zero#498: Also changes internal/tui/model.go’s Ctrl+B toggle behavior and adds a guard around when it can fire.

Suggested reviewers: Vasanthdev2004, gnanam1990, kevincodex1

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main TUI shortcut fix for non-empty composer input.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found a couple of blockers that need to be addressed before this is ready.

Findings

  • [P1] Fix the failing diff hygiene check
    internal/tui/keybinding_help_test.go:194
    The current PR is failing the Zero Review diff hygiene step because this added blank line contains trailing whitespace (git diff --check origin/main...HEAD reports trailing whitespace). Please remove the stray whitespace so the review/CI gate can pass before the code change is considered ready.

  • [P2] Keep remapped toggle shortcuts working while composing
    internal/tui/model.go:1172
    The new m.composerValue() == "" guard is applied to the whole m.keyMatch(...) result, so it disables user-configured toggleMouse and toggleSidebar bindings whenever the composer has text, not just the built-in Ctrl+E/Ctrl+B chords that conflict with readline navigation. For example, a user who remaps toggleMouse to a non-readline chord can no longer use that shortcut while drafting a prompt because this case no longer fires and the key falls through to the composer path. Please scope the bypass to the conflicting default chords, or otherwise preserve configured toggle bindings that do not need to fall through as composer navigation.

  • [P2] Link the approved parent issue for this community PR
    CONTRIBUTING.md:60
    The contribution policy requires community PRs to link an existing issue that already has the issue-approved label, but this PR body does not link any parent issue. Please add the approved issue reference, or get a maintainer to confirm an explicit exception, so the PR satisfies the repository's issue-first requirement.

… chords

Addresses jatmn's review:
- Removed a trailing-whitespace-only blank line in
  keybinding_help_test.go that was failing the diff-hygiene CI check.
- The composerValue() == "" guard on toggleMouse/toggleSidebar applied
  to the whole keyMatch(...) result, so a user who remaps either
  binding to a chord that doesn't conflict with readline navigation
  (Ctrl+E/Ctrl+B) lost that shortcut entirely while typing. The guard
  now only applies when the match came through the default chord;
  a remapped binding fires regardless of composer state.

Added TestRemappedToggleBindingsIgnoreComposerGuard covering both
bindings.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the update. I rechecked the changed paths and found issues that still need to be addressed.

Findings

  • [P2] Keep explicit default bindings on the composer bypass path
    internal/tui/model.go:1172
    The new guard now preserves remapped shortcuts, but it treats any non-zero configured binding as safe to fire while the composer has text. A user can still explicitly put the documented defaults in config, for example toggleMouse: "ctrl+e" or toggleSidebar: "ctrl+b"; resolveKeyBindings keeps those as non-zero bindings, so the !m.keyBindings.toggleMouse.isZero() / !m.keyBindings.toggleSidebar.isZero() side of the condition is true and Ctrl+E/Ctrl+B once again toggle mouse/sidebar instead of reaching the composer readline handlers. Please distinguish conflicting default chords from genuinely non-conflicting remaps, and add coverage for explicit ctrl+e / ctrl+b config values while composing.

Addresses jatmn's follow-up review: the prior fix checked
!b.isZero() to decide whether a binding is a genuine, non-conflicting
remap, but a user can explicitly configure toggleMouse: "ctrl+e" or
toggleSidebar: "ctrl+b" (the same chord as the built-in default).
parseBinding gives that a non-zero parsedBinding, so isZero() alone
wrongly treated it as safe to bypass the composer guard, letting
Ctrl+E/Ctrl+B hijack keystrokes mid-sentence again.

requiresEmptyComposer now compares the resolved binding against the
canonical default chord directly (parsedBinding is a plain comparable
struct), so both "unset" and "explicitly re-affirmed to the same
chord" require an empty composer; only a binding that resolves to a
genuinely different chord bypasses it.

Added TestExplicitDefaultChordConfigStillRequiresEmptyComposer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/tui/model.go (1)

1172-1172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: dedupe the composer-gate expression.

The !requiresEmptyComposer(binding, defaultChord) || m.composerValue() == "" pattern is repeated verbatim for the mouse and sidebar cases. A tiny helper would remove the duplication and make each case line easier to scan.

♻️ Optional helper extraction
+// canFireComposerGatedToggle reports whether a toggle bound to b (with
+// hardcoded conflicting default) may fire given the current composer text.
+func canFireComposerGatedToggle(b parsedBinding, conflicting parsedBinding, composerEmpty bool) bool {
+	return !requiresEmptyComposer(b, conflicting) || composerEmpty
+}
-		case m.keyMatch(m.keyBindings.toggleMouse, msg, func(tea.KeyMsg) bool { return keyCtrl(msg, 'e') }) && (!requiresEmptyComposer(m.keyBindings.toggleMouse, defaultToggleMouseChord) || m.composerValue() == ""):
+		case m.keyMatch(m.keyBindings.toggleMouse, msg, func(tea.KeyMsg) bool { return keyCtrl(msg, 'e') }) && canFireComposerGatedToggle(m.keyBindings.toggleMouse, defaultToggleMouseChord, m.composerValue() == ""):

Also applies to: 1377-1377

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/model.go` at line 1172, The composer-gate check is duplicated in
the toggleMouse and sidebar key handling cases, making the switch harder to
scan. Extract the repeated `!requiresEmptyComposer(... ) || m.composerValue() ==
""` logic into a small helper near `m.keyMatch`/`requiresEmptyComposer`, then
use that helper in both cases so the `model.go` switch branches stay concise and
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/tui/model.go`:
- Line 1172: The composer-gate check is duplicated in the toggleMouse and
sidebar key handling cases, making the switch harder to scan. Extract the
repeated `!requiresEmptyComposer(... ) || m.composerValue() == ""` logic into a
small helper near `m.keyMatch`/`requiresEmptyComposer`, then use that helper in
both cases so the `model.go` switch branches stay concise and consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1038f305-0828-44d5-aaaa-b62ed171130e

📥 Commits

Reviewing files that changed from the base of the PR and between 7bffc36 and 27239e9.

📒 Files selected for processing (3)
  • internal/tui/keybinding_help_test.go
  • internal/tui/keybindings.go
  • internal/tui/model.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/tui/keybinding_help_test.go

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CodeRabbit nitpick: the toggleMouse/toggleSidebar dispatch cases both
repeated !requiresEmptyComposer(...) || m.composerValue() == "" inline.
Extracted canFireComposerGatedToggle so both cases share one
expression.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the update. I rechecked the previously discussed paths and do not see any remaining actionable issues from my side.

@Vasanthdev2004 LGTM

@kevincodex1 kevincodex1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@kevincodex1
kevincodex1 merged commit c7346fb into Gitlawb:main Jul 8, 2026
7 checks passed
yanalialiuk pushed a commit to yanalialiuk/zero that referenced this pull request Jul 8, 2026
…composer is non-empty (Gitlawb#576)

* fix(tui): bypass toggleSidebar and toggleMouse global shortcuts when composer is non-empty

Allows standard readline/Emacs cursor movement keybindings (Ctrl+B and Ctrl+E) to work inside the prompt composer when typing a prompt. This prevents these keys from being globally intercepted to toggle the sidebar or mouse mode when the composer is populated.

* fix(tui): remove trailing whitespace, scope composer guard to default chords

Addresses jatmn's review:
- Removed a trailing-whitespace-only blank line in
  keybinding_help_test.go that was failing the diff-hygiene CI check.
- The composerValue() == "" guard on toggleMouse/toggleSidebar applied
  to the whole keyMatch(...) result, so a user who remaps either
  binding to a chord that doesn't conflict with readline navigation
  (Ctrl+E/Ctrl+B) lost that shortcut entirely while typing. The guard
  now only applies when the match came through the default chord;
  a remapped binding fires regardless of composer state.

Added TestRemappedToggleBindingsIgnoreComposerGuard covering both
bindings.

* fix(tui): treat an explicit default-chord config the same as unset

Addresses jatmn's follow-up review: the prior fix checked
!b.isZero() to decide whether a binding is a genuine, non-conflicting
remap, but a user can explicitly configure toggleMouse: "ctrl+e" or
toggleSidebar: "ctrl+b" (the same chord as the built-in default).
parseBinding gives that a non-zero parsedBinding, so isZero() alone
wrongly treated it as safe to bypass the composer guard, letting
Ctrl+E/Ctrl+B hijack keystrokes mid-sentence again.

requiresEmptyComposer now compares the resolved binding against the
canonical default chord directly (parsedBinding is a plain comparable
struct), so both "unset" and "explicitly re-affirmed to the same
chord" require an empty composer; only a binding that resolves to a
genuinely different chord bypasses it.

Added TestExplicitDefaultChordConfigStillRequiresEmptyComposer.

* refactor(tui): dedupe the composer-gated toggle check

CodeRabbit nitpick: the toggleMouse/toggleSidebar dispatch cases both
repeated !requiresEmptyComposer(...) || m.composerValue() == "" inline.
Extracted canFireComposerGatedToggle so both cases share one
expression.
@euxaristia
euxaristia deleted the fix/tui-cursor-key-interception branch July 17, 2026 21:05
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