Skip to content

Fix browser focus after dismissing the command palette by click - #990

Merged
austinywang merged 4 commits into
mainfrom
issue-983-browser-unfocusable-after-cmdr
Mar 6, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-983-browser-unfocusable-after-cmdr

Conversation

@austinywang

@austinywang austinywang commented Mar 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • detect the underlying responder when the command palette backdrop is clicked
  • restore focus to the clicked browser or terminal pane instead of the stale pre-palette target
  • add a browser-pane UI regression test for dismissing the Cmd+R rename palette by clicking browser content

Testing

  • built and launched a tagged Debug app via ./scripts/reload.sh --tag issue-983-browser-unfocusable-after-cmdr
  • UI tests not run locally per repo policy; rely on CI

Summary by cubic

Fixes focus after dismissing the command palette by clicking the backdrop. We hit-test the click, map it to the underlying browser or terminal, and restore focus to that exact pane (Linear 983).

  • Bug Fixes
    • Backdrop click uses a zero-distance DragGesture and overlay hit-test, resolving the owning WKWebView/terminal via the responder chain with fallbacks to window portal registries.
    • Dismiss now accepts a preferred focus target computed from the click and searches across workspaces to map web views to panels, avoiding the stale pre-palette target.
    • Added accessibility IDs for the search/rename fields, backdrop, and browser pane content; new UI test verifies clicking browser content dismisses the palette and keeps browser focus.

Written for commit 44910d0. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Enhanced command palette focus behavior when clicking the backdrop to dismiss.
  • Improvements

    • Improved focus restoration after command palette dismissal.
    • Refined backdrop click handling for better interaction flow.
  • Accessibility

    • Added accessibility identifiers to command palette fields for better UI testing support.
  • Tests

    • Added test coverage for command palette dismissal and focus behavior.

@vercel

vercel Bot commented Mar 6, 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 Mar 6, 2026 5:31am

@coderabbitai

coderabbitai Bot commented Mar 6, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements comprehensive command palette backdrop focus handling by adding responder discovery mechanisms, focus target derivation helpers across terminal and browser contexts, and focus restoration logic. A new DragGesture replaces the backdrop tap handling to route clicks through the new handler, with accessibility identifiers added for UI testing.

Changes

Cohort / File(s) Summary
Command Palette Backdrop Focus System
Sources/ContentView.swift
Added window overlay responder discovery (underlyingResponder(atWindowPoint:)), responder ownership traversal (commandPaletteOwningWebView(for:)), comprehensive backdrop navigation helpers for deriving focus targets from terminal/browser/panel responders (commandPaletteBackdropFocusTarget(atWindowPoint:in:), commandPaletteBrowserFocusTarget(for:), etc.), and focus restoration flow (restoreCommandPaletteFocus, handleCommandPaletteBackdropClick(atContentPoint:)). Extended dismissCommandPalette to accept optional preferred focus target. Replaced backdrop onTapGesture with DragGesture for click routing. Added accessibility identifiers to search and rename fields.
Backdrop Dismissal Test Coverage
cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
Added testClickingBrowserDismissesCommandPaletteAndKeepsBrowserFocus() to validate that clicking the browser pane dismisses the rename command palette while maintaining focus on the correct browser pane.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as Command Palette UI
    participant Handler as Backdrop Click Handler
    participant Responder as Responder Discovery
    participant Focus as Focus Target Derivation
    participant ContentView as ContentView

    User->>UI: Click backdrop
    UI->>Handler: DragGesture triggered
    Handler->>Responder: underlyingResponder(atWindowPoint:)
    Responder->>Responder: Disable container capture, hitTest on theme frame
    Responder-->>Handler: NSResponder at click point
    Handler->>Focus: commandPaletteBackdropFocusTarget(for:)
    Focus->>Focus: Traverse responder chain & superviews
    Focus->>Focus: Check terminal/browser/panel contexts
    Focus-->>Handler: CommandPaletteRestoreFocusTarget
    Handler->>ContentView: dismissCommandPalette(preferredFocusTarget:)
    ContentView->>ContentView: restoreCommandPaletteFocus()
    ContentView-->>User: Palette dismissed, focus restored
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A palette dismissed with a graceful tap,
Focus flows back through the responder map,
From browser to terminal, no focus shall slip,
The backdrop now knows which responder to flip! 🎯

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
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.
Description check ❓ Inconclusive PR description includes summary of changes and testing approach, but lacks demo video link and incomplete checklist items. Add a demo video link and complete the checklist by confirming all items were done or marking them as not applicable.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing browser focus restoration when dismissing the command palette via backdrop click.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-983-browser-unfocusable-after-cmdr

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

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmuxUITests/BrowserPaneNavigationKeybindUITests.swift">

<violation number="1" location="cmuxUITests/BrowserPaneNavigationKeybindUITests.swift:325">
P2: Avoid fixed `RunLoop` sleeps in this UI test; wait until the rename field actually disappears to reduce flakiness and unnecessary delay.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread cmuxUITests/BrowserPaneNavigationKeybindUITests.swift Outdated
@greptile-apps

greptile-apps Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a focus regression where dismissing the command palette by clicking a browser or terminal pane behind it would restore focus to the stale pre-palette target instead of the pane the user actually clicked. The fix intercepts backdrop clicks via a DragGesture (replacing .onTapGesture to capture click coordinates), performs a hit-test against the underlying view hierarchy (temporarily disabling capturesMouseEvents on the overlay), and resolves the clicked pane through the browser/terminal portal registries. A new dismissCommandPalette(restoreFocus:preferredFocusTarget:) overload threads the resolved focus target through the existing restore machinery.

Key changes:

  • underlyingResponder(atWindowPoint:) in WindowCommandPaletteOverlayController — performs a pass-through hit-test by briefly clearing capturesMouseEvents
  • commandPaletteOwningWebView(for:) — new free function that walks a responder's view hierarchy and responder chain to find an owning WKWebView; has a logic issue where the NSTextView delegate branch returns early on nil, silently skipping the nextResponder chain fallback
  • handleCommandPaletteBackdropClick + commandPaletteBackdropFocusTarget — new method group wiring the gesture → hit-test → focus-target pipeline
  • CommandPaletteBackdrop, CommandPaletteRenameField, CommandPaletteSearchField — accessibility identifiers added, enabling the new UI test
  • New UI test testClickingBrowserDismissesCommandPaletteAndKeepsBrowserFocus validates the fix end-to-end; uses a hardcoded normalized window coordinate to target the browser pane, which is fragile to layout changes

Confidence Score: 3/5

  • Generally safe to merge; the logic bug in commandPaletteOwningWebView could cause focus to fall back to the stale pre-palette target in edge cases involving NSTextView with an NSView delegate, partially defeating the fix's goal.
  • The architecture is well-structured and the happy path (click on a WKWebView or terminal surface) will work correctly. However, the early return in commandPaletteOwningWebView when the NSTextView delegate path returns nil means any responder that requires the nextResponder chain walk after a failed delegate detour will not be detected, regressing to the stale focus target. The hardcoded normalized coordinate in the UI test is fragile but only affects test reliability, not production behaviour.
  • Sources/ContentView.swift — specifically the commandPaletteOwningWebView function around the NSTextView delegate branch (lines 1160–1163).

Important Files Changed

Filename Overview
Sources/ContentView.swift Adds backdrop-click handling for the command palette: detects the underlying responder via a temporary capturesMouseEvents = false hit-test, walks the view/responder/portal hierarchies to identify the clicked browser or terminal pane, and passes that as the preferred focus target to the overloaded dismissCommandPalette. Has a logic bug in commandPaletteOwningWebView where the NSTextView delegate branch returns early on failure, bypassing the nextResponder chain walk.
cmuxUITests/BrowserPaneNavigationKeybindUITests.swift Adds a regression UI test verifying that clicking the browser pane while the rename command palette is open dismisses the palette and restores focus to the correct browser panel. Test structure is consistent with existing tests; uses hardcoded normalized window coordinates (0.82, 0.78) to target the browser pane, which is fragile to layout changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant Backdrop as CommandPalette Backdrop (DragGesture)
    participant CV as ContentView
    participant OC as WindowCommandPaletteOverlayController
    participant BR as BrowserWindowPortalRegistry
    participant TR as TerminalWindowPortalRegistry

    User->>Backdrop: click (DragGesture.onEnded)
    Backdrop->>CV: handleCommandPaletteBackdropClick(atContentPoint:)
    CV->>CV: commandPaletteBackdropFocusTarget(atContentPoint:)
    CV->>OC: underlyingResponder(atWindowPoint:)
    OC->>OC: capturesMouseEvents = false
    OC->>OC: themeFrame.hitTest(pointInTheme)
    OC-->>CV: NSResponder?
    alt Responder found
        CV->>CV: commandPaletteBackdropFocusTarget(for responder)
        CV->>CV: commandPaletteOwningWebView(for:) or cmuxOwningGhosttyView(for:)
        CV-->>CV: CommandPaletteRestoreFocusTarget?
    else No responder hit
        CV->>BR: webViewAtWindowPoint(_:in:)
        BR-->>CV: WKWebView?
        alt WebView found
            CV->>CV: commandPaletteBrowserFocusTarget(for:)
        else No WebView
            CV->>TR: terminalViewAtWindowPoint(_:in:)
            TR-->>CV: TerminalView?
        end
    end
    CV->>CV: dismissCommandPalette(restoreFocus: true, preferredFocusTarget:)
    CV->>CV: restoreCommandPaletteFocus(target:attemptsRemaining:)
    CV-->>User: Focus restored to clicked pane
Loading

Last reviewed commit: 82d6086

Comment thread Sources/ContentView.swift
Comment thread cmuxUITests/BrowserPaneNavigationKeybindUITests.swift Outdated
Comment thread Sources/ContentView.swift Outdated

@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)
cmuxUITests/BrowserPaneNavigationKeybindUITests.swift (1)

322-326: Consider making the click target more robust.

The normalized offset (dx: 0.82, dy: 0.78) assumes a specific window layout where the browser pane occupies that region. If the layout changes (e.g., different pane ratios, window sizes), this could click the wrong element or miss the browser entirely, causing test flakiness.

Consider using an accessibility identifier on the browser pane content area and querying it directly, similar to how BrowserOmnibarTextField and CommandPaletteRenameField are used elsewhere in this file.

Also, the 0.2s hardcoded delay after clicking may be insufficient or excessive depending on system load. Consider replacing it with a waitForDataMatch predicate if the app emits a signal when focus changes, or at minimum document why this delay is necessary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift` around lines 322 -
326, Replace the brittle coordinate click and fixed delay with a direct hit on
the browser pane element by querying it via an accessibility identifier (e.g.,
add/use an identifier like "BrowserContentArea" and locate it via
app.otherElements["BrowserContentArea"]) and call its click; then replace the
RunLoop delay with an explicit wait that asserts the command palette rename
field disappears (use a wait/predicate against renameField, e.g.,
XCTAssertFalse(renameField.waitForExistence(timeout: X)) or use
NSPredicate-based expectation) so the test targets BrowserPane content reliably
and waits deterministically for the focus change instead of relying on
coordinates and a hardcoded 0.2s sleep.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift`:
- Around line 322-326: Replace the brittle coordinate click and fixed delay with
a direct hit on the browser pane element by querying it via an accessibility
identifier (e.g., add/use an identifier like "BrowserContentArea" and locate it
via app.otherElements["BrowserContentArea"]) and call its click; then replace
the RunLoop delay with an explicit wait that asserts the command palette rename
field disappears (use a wait/predicate against renameField, e.g.,
XCTAssertFalse(renameField.waitForExistence(timeout: X)) or use
NSPredicate-based expectation) so the test targets BrowserPane content reliably
and waits deterministically for the focus change instead of relying on
coordinates and a hardcoded 0.2s sleep.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 55bf0a7a-a466-45b9-8ceb-5582e790fb3a

📥 Commits

Reviewing files that changed from the base of the PR and between dd2eeae and 82d6086.

📒 Files selected for processing (2)
  • Sources/ContentView.swift
  • cmuxUITests/BrowserPaneNavigationKeybindUITests.swift

@austinywang
austinywang merged commit 138ed0e into main Mar 6, 2026
12 checks passed
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…-unfocusable-after-cmdr

Fix browser focus after dismissing the command palette by click

This branch was successfully deployed

1 active deployment
Preview — 44910d03 Deployed Mar 6, 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