Skip to content

Fix command palette arrow keys and no-match flash - #3698

Merged
lawrencecchen merged 7 commits into
mainfrom
debug-command-palette-arrows
May 7, 2026
Merged

lawrencecchen merged 7 commits into
mainfrom
debug-command-palette-arrows

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Route horizontal arrow keys back to the command palette field editor before app shortcut routing consumes them.
  • Keep the command palette no-match empty state stable while a same-corpus search is pending.
  • Add regression tests for both the arrow handling and in-place no-match edit flash.

Verification

  • Red check on test-only commit: selected command palette tests failed as expected.
  • Green check: xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination platform=macOS -derivedDataPath /tmp/cmux-palette-green-check -only-testing:cmuxTests/CommandPaletteShortcutCustomizationTests/testWindowPerformKeyEquivalentRoutesHorizontalArrowsToCommandPaletteFieldEditor -only-testing:cmuxTests/CommandPaletteSearchEngineTests/testPendingEmptyStateIsNotPreservedWhenSearchIsNotPending -only-testing:cmuxTests/CommandPaletteSearchEngineTests/testPendingEmptyStateIsPreservedForSameResolvedNoMatchQuery -only-testing:cmuxTests/CommandPaletteSearchEngineTests/testPendingEmptyStateIsPreservedForSameScopeNoMatchInPlaceEdit -only-testing:cmuxTests/CommandPaletteSearchEngineTests/testPendingEmptyStateIsNotPreservedWhenResolvedResultsMayBeStale test

Note

Medium Risk
Changes macOS performKeyEquivalent routing and command palette search/empty-state behavior, which could affect global keyboard handling and UI responsiveness if edge cases are missed.

Overview
Fixes command palette keyboard routing so left/right arrow keys are forwarded to the palette’s focused field editor (when the visible overlay actually owns the field editor and there’s no IME marked text), preventing app-level shortcut routing from swallowing them.

Stabilizes command palette results rendering and the no-match empty state by rebuilding the results container on scope-only transitions and keeping the empty-state message pinned while a same-scope search is pending; also tightens async search result application guards to avoid stale updates.

Adds regression tests covering horizontal arrow forwarding/ownership guards and the pending no-match empty-state behavior.

Reviewed by Cursor Bugbot for commit 59c9f2b. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes command palette horizontal arrow routing and removes the empty‑state flicker. Arrows only reach the palette when its overlay is presented, and results stay stable during same‑scope pending searches.

  • Bug Fixes
    • Route left/right arrows (with allowed modifiers) to the command palette field editor when it’s first responder and not in IME; require the editor to belong to a presented overlay (not hidden/transparent) so non‑palette editors aren’t affected.
    • Keep the “no match” message steady for any same‑scope pending search, including in‑place edits; results list identity is now scope‑only to prevent stale rows and flicker.
    • Apply results only when the request, scope, matching query, and fingerprint still match to avoid stale updates.
    • Added regression tests for horizontal arrow routing, transparent overlay ownership, non‑palette arrow ownership, and same‑scope empty‑state preservation.

Written for commit 59c9f2b. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Command palette keyboard handling: left/right arrows now behave correctly while editing, respect palette visibility, and won’t interfere with other text fields
    • More stable handling of empty search results and fewer unnecessary result-list rebuilds during scope/query changes
  • Tests

    • Expanded tests validating command-palette arrow routing, visibility cases, and search-result state transitions

@vercel

vercel Bot commented May 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 7, 2026 0:25am
cmux-staging Building Building Preview, Comment May 7, 2026 0:25am

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented May 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves command-palette keyboard interaction by adding horizontal arrow key forwarding to the field editor and refining empty-state preservation during search. It implements field-editor ownership detection to route arrow events correctly, adds re-entrancy guards, simplifies list-identity computation, and updates search-state logic to depend on resolved-results emptiness rather than query fingerprints.

Changes

Command Palette Arrow Routing & Search State Refinement

Layer / File(s) Summary
Arrow Dispatch Predicate
Sources/App/ShortcutRoutingSupport.swift
New shouldDispatchCommandPaletteHorizontalArrowViaFirstResponderKeyDown function gates horizontal arrow (keyCode 123/124) dispatch on responder identity, IME marked-text absence, and permitted modifier flags (none, shift, option, option+shift, cmd, cmd+shift).
Field Editor Ownership Detection
Sources/AppDelegate.swift
New NSWindow.cmuxCommandPaletteOwnsFieldEditor(_:in:) helper determines whether an NSTextView field editor belongs to the command-palette overlay by traversing ownership and checking visibility/alpha.
Arrow Routing Integration & Re-entrancy
Sources/AppDelegate.swift
Global cmuxCommandPaletteArrowForwardingDepth counter guards command-palette arrow forwarding. In cmux_performKeyEquivalent, firstResponderIsCommandPaletteFieldEditor is computed via the ownership helper, and a guarded path routes qualifying arrow events to firstResponder.keyDown with depth protection.
Search State & List Identity
Sources/ContentView.swift
New commandPaletteListIdentity(for:) helper replaces inline scope+query string construction. Empty-state preservation now returns true based only on resolvedResultsAreEmpty when visible list is empty, removing query-fingerprint comparison. Call sites pass resolvedResultsAreEmpty: cachedCommandPaletteResults.isEmpty.
Arrow Routing Tests
cmuxTests/CommandPaletteShortcutCustomizationTests.swift
withCommandPaletteFieldEditor helper now passes the field editor instance to test closures. Field editor subclass adds keyDownKeyCodes property and keyDown(with:) override to track received key codes. New test testWindowPerformKeyEquivalentRoutesHorizontalArrowsToCommandPaletteFieldEditor asserts left/right arrow events are routed and recorded. Existing tests updated for new helper signature.
Search State Tests
cmuxTests/CommandPaletteSearchEngineTests.swift
Test coverage reorganized: "pending empty state is not preserved" repurposed for non-pending search (isSearchPending: false). New test asserts empty state preserved for same scope on in-place edit ("no match"). "Resolved results may be stale" test updated to depend on resolvedResultsAreEmpty and fingerprint/scope flags instead of query-comparison assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#1417: Both PRs modify command-palette keyboard routing—adding/AppDelegate-level key-event forwarding for arrow keys and accompanying tests.
  • manaflow-ai/cmux#847: Both PRs modify command-palette key-routing in AppDelegate/NSWindow and add overlay/container/field-editor visibility helpers.
  • manaflow-ai/cmux#717: Both PRs modify NSWindow.cmux_performKeyEquivalent to add passthrough branches that forward key events to focused subviews.

Poem

🐰 Whiskers twitch as arrows fly

Through the palette, swift and spry
Empty states no longer fret
When resolved results reset
Ownership guards keep routes true ✨


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces NSLock and Task.sleep(150ms) in production code, violating swift-blocking-runtime rules against manual locks and sleeps without real signals. Replace NSLock with MainActor isolation; replace Task.sleep with real completion signal or callback.
Cmux Swift File And Package Boundaries ❌ Error Budget TSV values don't match actual files. AppDelegate: 14705 vs 14636 budget (+69); ShortcutRoutingSupport: 666 vs 645 (+21). Violates rule against expanding budgets without reduction. Update TSV to match actual sizes (14705, 666) or reduce files through refactoring to stay within budgets per rule.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (11 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: fixing command palette arrow key routing and eliminating the no-match empty state flicker, which align with the primary objectives of the PR.
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.
Cmux Swift Actor Isolation ✅ Passed Production changes do not violate Swift 6 actor isolation rules. Global mutable variables are accessed only from MainActor-bound NSWindow extension methods. Utility functions properly isolated.
Cmux No Hacky Sleeps ✅ Passed Check applies only to TypeScript, JavaScript, shell, and non-Swift runtime scripts. This PR modifies only Swift files (excluded from this rule; covered by swift-blocking-runtime.md instead).
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns. All additions are synchronous view hierarchy checks, key event routing, and tests. Uses AppKit APIs appropriately at callback boundaries.
Cmux Swift @Concurrent ✅ Passed All new functions are synchronous. The nonisolated static helper is correctly marked. No async functions, @concurrent annotations, or concurrent isolation issues found.
Cmux Swift Logging ✅ Passed No logging violations. New production functions contain no print, NSLog, debugPrint, or dump statements. Test print() calls are acceptable benchmarks.
Cmux Swiftui State Layout ✅ Passed PR introduces only utility functions and AppKit helpers. No new @Observable, @Published, @StateObject, @EnvironmentObject declarations, no problematic GeometryReader, and no render-time mutations.
Cmux Architecture Rethink ✅ Passed Follows existing browser arrow pattern. No timing, observers, or duplicate owners. Pure helpers with clear state ownership. Small correctness fix matching allowed cases criteria.
Description check ✅ Passed The pull request description covers all required sections: a clear summary of changes, verification approach with test results, and testing details. However, the Demo Video and Review Trigger sections are missing.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch debug-command-palette-arrows

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 and usage tips.

@greptile-apps

greptile-apps Bot commented May 7, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two command palette UX bugs: horizontal arrow keys consumed by performKeyEquivalent before reaching the palette's field editor, and a flash of the "no match" empty state when the user edits a no-result query in place.

  • Arrow routing: performKeyEquivalent now detects when the first responder is the command palette's own field editor and forwards left/right arrow events directly to keyDown, with a re-entrancy guard. The ownership check correctly short-circuits when the field editor belongs to a different control.
  • Empty-state stability: commandPaletteListIdentity is now scope-only (not scope:query), preventing SwiftUI list rebuilds on every keystroke. commandPaletteShouldPreserveEmptyStateWhileSearchPending now returns true for any same-corpus pending search (not just prefix refinements), removing the mid-edit flash. The dead currentMatchingQuery/resolvedMatchingQuery parameters are cleaned up, and the stale-result guard in scheduleCommandPaletteResultsRefresh now explicitly names its condition.

Confidence Score: 5/5

Safe to merge — both fixes are well-scoped with no regressions identified.

The arrow-routing change adds a narrowly-targeted branch inside performKeyEquivalent gated on ownership verification, following the established re-entrancy depth counter pattern. The empty-state logic correctly broadens preservation to any same-corpus pending search while resolvedResultsAreEmpty prevents false positives. All new code paths have regression tests and the previously flagged dead-parameter issue is resolved.

No files require special attention.

Important Files Changed

Filename Overview
Sources/App/ShortcutRoutingSupport.swift Adds shouldDispatchCommandPaletteHorizontalArrowViaFirstResponderKeyDown alongside the existing browser-arrow predicate; modifier-flag filtering is consistent with the browser variant.
Sources/AppDelegate.swift Adds field-editor ownership helpers and wires the new arrow-routing predicate into performKeyEquivalent with a re-entrancy depth guard. Fallback DFS traversal is only reached when no registered owner is found, keeping the hot path cheap.
Sources/ContentView.swift Scope-only list identity prevents SwiftUI list rebuilds on query changes; commandPaletteShouldPreserveEmptyStateWhileSearchPending broadened to any same-corpus pending search and dead query parameters removed.
cmuxTests/CommandPaletteSearchEngineTests.swift Test suite updated to match revised function signature; adds coverage for the non-prefix in-place edit case and the previously-untested scope-mismatch staleness paths.
cmuxTests/CommandPaletteShortcutCustomizationTests.swift Refactors test helpers into a two-layer structure; adds three new routing tests; moves setCommandPaletteVisible(false) into defer so it runs after rather than before the test body.

Sequence Diagram

sequenceDiagram
    participant User
    participant NSWindow as NSWindow.performKeyEquivalent
    participant OwnerCheck as cmuxCommandPaletteOwnsFieldEditor
    participant Guard as Re-entrancy Guard
    participant FieldEditor as Palette Field Editor

    User->>NSWindow: Left/Right arrow key event
    NSWindow->>OwnerCheck: firstResponder as NSTextView?
    OwnerCheck-->>NSWindow: true (owner is inside palette overlay)
    NSWindow->>Guard: "cmuxCommandPaletteArrowForwardingDepth > 0?"
    Guard-->>NSWindow: false (not re-entrant)
    NSWindow->>NSWindow: "depth += 1"
    NSWindow->>FieldEditor: keyDown(with: event)
    NSWindow->>NSWindow: "depth -= 1 (defer)"
    NSWindow-->>User: return true (handled)
Loading

Reviews (4): Last reviewed commit: "fix: require presented command palette o..." | Re-trigger Greptile

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Addressed Greptile's cleanup note in ba27e4e by removing the stale current/resolved matching-query parameters from the empty-state helper, call site, and tests.

Comment thread Sources/AppDelegate.swift Outdated
Comment on lines +13852 to +13860
if let ownerView = cmuxFieldEditorOwnerView(textView),
cmuxIsInsideCommandPaletteOverlay(ownerView) {
return true
}

guard let container = cmuxCommandPaletteOverlayContainer(in: window) else {
return false
}
return !container.isHidden && container.alphaValue > 0.001

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.

P1 When cmuxFieldEditorOwnerView returns a non-nil view that is not inside the palette overlay, the compound if let …, condition simply falls through rather than returning false. The code then reaches the container-visibility fallback and can return true solely because the palette container has alphaValue > 0.001 — even though the active field editor belongs to a completely different control (e.g., a browser search bar). This would silently steal horizontal arrow keys from that other field editor. The fix is to return immediately when an owner is positively identified as outside the overlay, reserving the container-visibility heuristic only for the case where the owner cannot be determined at all.

Suggested change
if let ownerView = cmuxFieldEditorOwnerView(textView),
cmuxIsInsideCommandPaletteOverlay(ownerView) {
return true
}
guard let container = cmuxCommandPaletteOverlayContainer(in: window) else {
return false
}
return !container.isHidden && container.alphaValue > 0.001
if let ownerView = cmuxFieldEditorOwnerView(textView) {
return cmuxIsInsideCommandPaletteOverlay(ownerView)
}
// Owner lookup failed — fall back to container visibility as a best-effort heuristic.
guard let container = cmuxCommandPaletteOverlayContainer(in: window) else {
return false
}
return !container.isHidden && container.alphaValue > 0.001

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 525bddf. The owner check now returns false immediately when the field editor owner is outside the command palette overlay, and 3b5c856 adds coverage for that false-positive path.

— Claude Code

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ba27e4e. Configure here.

Comment thread Sources/AppDelegate.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 7, 2026

@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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@cmuxTests/CommandPaletteShortcutCustomizationTests.swift`:
- Around line 417-430: The test improperly adds overlayContainer and
outsideOwnerView as siblings under searchRoot causing
cmuxIsInsideCommandPaletteOverlay to vacuously return false; instead, mirror
withVisibleCommandPaletteOverlay by adding overlayContainer and outsideOwnerView
as subviews of contentView so the upward traversal can encounter the overlay,
and ensure the palette is visible during the check (do not call
setCommandPaletteVisible(false) before asserting
cmuxCommandPaletteOwnsFieldEditor) so the test verifies detection logic for a
visible overlay.

In `@Sources/AppDelegate.swift`:
- Around line 13845-13860: The cmuxCommandPaletteOwnsFieldEditor(_:, in:) helper
currently returns true when cmuxFieldEditorOwnerView(_:) finds an owner even if
the overlay is hidden; update this function so that after obtaining ownerView
via cmuxFieldEditorOwnerView(textView) you also retrieve the overlay container
using cmuxCommandPaletteOverlayContainer(in: window) and only return
cmuxIsInsideCommandPaletteOverlay(ownerView) if that container exists and is
visible (not hidden and alphaValue > 0.001); otherwise fall through to the
existing container-visibility check and return false when the overlay is not
presented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: af9c4c80-7315-473d-9cb3-8abd8dcb8eed

📥 Commits

Reviewing files that changed from the base of the PR and between ba27e4e and 525bddf.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • cmuxTests/CommandPaletteShortcutCustomizationTests.swift

Comment thread cmuxTests/CommandPaletteShortcutCustomizationTests.swift Outdated
Comment thread Sources/AppDelegate.swift
@lawrencecchen
lawrencecchen dismissed coderabbitai[bot]’s stale review May 7, 2026 12:33

Stale CodeRabbit review. The two requested changes were addressed in 1e0de70 and 59c9f2b, both inline threads are resolved, and the latest CodeRabbit check passed on 59c9f2b.

@lawrencecchen
lawrencecchen merged commit 7142e31 into main May 7, 2026
28 checks passed
@lawrencecchen
lawrencecchen deleted the debug-command-palette-arrows branch May 7, 2026 12:33

This branch was successfully deployed

1 active deployment
Preview – cmux — 59c9f2bb Deployed May 7, 2026 by vercel[bot]
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.

1 participant